diff --git a/services/web/.eslintignore b/services/web/.eslintignore index 4f0838e9a8..c4910a4e86 100644 --- a/services/web/.eslintignore +++ b/services/web/.eslintignore @@ -1,12 +1,7 @@ -app/js -modules/**/app/js modules/**/scripts -modules/*/index.js public/js public/minjs modules/**/public/js -test/**/js -modules/**/test/**/js -app.js +test/unit_frontend/js webpack.config.* -karma.conf.js \ No newline at end of file +karma.conf.js diff --git a/services/web/.gitignore b/services/web/.gitignore index f0c799e2dc..c5fc0338cc 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -36,13 +36,6 @@ Thumbs.db node_modules/* data/* -app.js -app.js.map -app/js/* -test/unit/js/* -test/unit_frontend/js/* -test/smoke/js/* -test/acceptance/js/* cookies.txt requestQueueWorker.js TpdsWorker.js @@ -71,10 +64,11 @@ public/stylesheets/ieee-style*.css public/stylesheets/*.map public/minjs/ -Gemfile.lock - public/js/libs/require*.js +test/unit_frontend/js/ + +Gemfile.lock *.swp .DS_Store @@ -84,8 +78,3 @@ docker-shared.yml config/*.coffee modules/**/Makefile - -modules/*/index.js -modules/**/app/js/* -modules/**/test/unit/js/* -modules/**/test/acceptance/js/* diff --git a/services/web/.prettierignore b/services/web/.prettierignore index 4f0838e9a8..c4910a4e86 100644 --- a/services/web/.prettierignore +++ b/services/web/.prettierignore @@ -1,12 +1,7 @@ -app/js -modules/**/app/js modules/**/scripts -modules/*/index.js public/js public/minjs modules/**/public/js -test/**/js -modules/**/test/**/js -app.js +test/unit_frontend/js webpack.config.* -karma.conf.js \ No newline at end of file +karma.conf.js diff --git a/services/web/.vscode/settings.json b/services/web/.vscode/settings.json index 36a2567460..8cbded1a1b 100644 --- a/services/web/.vscode/settings.json +++ b/services/web/.vscode/settings.json @@ -2,7 +2,7 @@ "files.exclude": { "app/js": true, "public/js": true, - "test/**/js": true, + "test/unit_frontend/js": true, "node_modules": true, "data": true } diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee deleted file mode 100644 index 279875c648..0000000000 --- a/services/web/Gruntfile.coffee +++ /dev/null @@ -1,58 +0,0 @@ -fs = require "fs" -PackageVersions = require "./app/coffee/infrastructure/PackageVersions" -Settings = require "settings-sharelatex" -require('es6-promise').polyfill() - -module.exports = (grunt) -> - grunt.loadNpmTasks 'grunt-contrib-requirejs' - grunt.loadNpmTasks 'grunt-file-append' - - config = - - requirejs: - compile: - options: - optimize:"uglify2" - uglify2: - mangle: false - appDir: "public/js" - baseUrl: "./" - dir: "public/minjs" - inlineText: false - generateSourceMaps: true - preserveLicenseComments: false - paths: - "moment": "libs/#{PackageVersions.lib('moment')}" - "mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML" - "pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf" - "ace": "#{PackageVersions.lib('ace')}" - "fineuploader": "libs/#{PackageVersions.lib('fineuploader')}" - - skipDirOptimize: true - modules: [ - { - name: "main", - exclude: ["libraries"] - }, { - name: "ide", - exclude: ["pdfjs-dist/build/pdf", "libraries"] - },{ - name: "libraries" - },{ - name: "ace/mode-latex" - },{ - name: "ace/worker-latex" - } - - ] - - file_append: - default_options: files: [ { - append: '\n//ide.js is complete - used for automated testing' - input: 'public/minjs/ide.js' - output: 'public/minjs/ide.js' - }] - - grunt.initConfig config - grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append"] - diff --git a/services/web/Gruntfile.js b/services/web/Gruntfile.js new file mode 100644 index 0000000000..0104d5b536 --- /dev/null +++ b/services/web/Gruntfile.js @@ -0,0 +1,86 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const fs = require('fs') +const PackageVersions = require('./app/src/infrastructure/PackageVersions') +const Settings = require('settings-sharelatex') +require('es6-promise').polyfill() + +module.exports = function(grunt) { + grunt.loadNpmTasks('grunt-contrib-requirejs') + grunt.loadNpmTasks('grunt-file-append') + + const config = { + requirejs: { + compile: { + options: { + optimize: 'uglify2', + uglify2: { + mangle: false + }, + appDir: 'public/js', + baseUrl: './', + dir: 'public/minjs', + inlineText: false, + generateSourceMaps: true, + preserveLicenseComments: false, + paths: { + moment: `libs/${PackageVersions.lib('moment')}`, + mathjax: '/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML', + 'pdfjs-dist/build/pdf': `libs/${PackageVersions.lib('pdfjs')}/pdf`, + ace: `${PackageVersions.lib('ace')}`, + fineuploader: `libs/${PackageVersions.lib('fineuploader')}` + }, + + skipDirOptimize: true, + modules: [ + { + name: 'main', + exclude: ['libraries'] + }, + { + name: 'ide', + exclude: ['pdfjs-dist/build/pdf', 'libraries'] + }, + { + name: 'libraries' + }, + { + name: 'ace/mode-latex' + }, + { + name: 'ace/worker-latex' + } + ] + } + } + }, + + file_append: { + default_options: { + files: [ + { + append: '\n//ide.js is complete - used for automated testing', + input: 'public/minjs/ide.js', + output: 'public/minjs/ide.js' + } + ] + } + } + } + + grunt.initConfig(config) + return grunt.registerTask( + 'compile:minify', + 'Concat and minify the client side js', + ['requirejs', 'file_append'] + ) +} diff --git a/services/web/Makefile b/services/web/Makefile index b33a2c64c2..ccf5894cb1 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -16,23 +16,16 @@ MODULE_DIRS := $(shell find modules -mindepth 1 -maxdepth 1 -type d -not -name ' MODULE_MAKEFILES := $(MODULE_DIRS:=/Makefile) MODULE_NAME=$(shell basename $(MODULE)) -COFFEE := node_modules/.bin/coffee -m $(COFFEE_OPTIONS) -COFFEE := node_modules/.bin/coffee $(COFFEE_OPTIONS) BABEL := node_modules/.bin/babel GRUNT := node_modules/.bin/grunt LESSC := node_modules/.bin/lessc CLEANCSS := node_modules/.bin/cleancss -APP_COFFEE_FILES := $(shell find app/coffee -name '*.coffee') -FRONT_END_SRC_FILES := $(shell find public/src -name '*.js') -TEST_COFFEE_FILES := $(shell find test/*/coffee -name '*.coffee') -TEST_SRC_FILES := $(shell find test/*/src -name '*.js') -MODULE_MAIN_SRC_FILES := $(shell find modules -type f -wholename '*main/index.js') -MODULE_IDE_SRC_FILES := $(shell find modules -type f -wholename '*ide/index.js') -COFFEE_FILES := app.coffee $(APP_COFFEE_FILES) $(FRONT_END_COFFEE_FILES) $(TEST_COFFEE_FILES) -SRC_FILES := $(FRONT_END_SRC_FILES) $(TEST_SRC_FILES) -JS_FILES := $(subst coffee,js,$(COFFEE_FILES)) -OUTPUT_SRC_FILES := $(subst src,js,$(SRC_FILES)) +SRC_FILES := $(shell find public/src -name '*.js') +DIST_FILES := $(subst src,js,$(SRC_FILES)) +MAIN_SRC_FILES := $(shell find modules -type f -wholename '*main/index.js') +IDE_SRC_FILES := $(shell find modules -type f -wholename '*ide/index.js') + LESS_FILES := $(shell find public/stylesheets -name '*.less') LESSC_COMMON_FLAGS := --source-map --autoprefix="last 2 versions, ie >= 10" CLEANCSS_FLAGS := --s0 --source-map @@ -49,35 +42,15 @@ CSS_OL_IEEE_FILE := public/stylesheets/ieee-style.css CSS_FILES := $(CSS_SL_FILE) $(CSS_OL_FILE) $(CSS_OL_LIGHT_FILE) $(CSS_OL_IEEE_FILE) # The automatic variable $(@D) is the target directory name -app.js: app.coffee - $(COFFEE) --compile -o $(@D) $< - -app/js/%.js: app/coffee/%.coffee - @mkdir -p $(@D) - $(COFFEE) --compile -o $(@D) $< - public/js/%.js: public/src/%.js @mkdir -p $(@D) $(BABEL) $< --out-file $@ -test/unit/js/%.js: test/unit/coffee/%.coffee - @mkdir -p $(@D) - $(COFFEE) --compile -o $(@D) $< - -test/acceptance/js/%.js: test/acceptance/coffee/%.coffee - @mkdir -p $(@D) - $(COFFEE) --compile -o $(@D) $< - test/unit_frontend/js/%.js: test/unit_frontend/src/%.js @mkdir -p $(@D) $(BABEL) $< --out-file $@ -test/smoke/js/%.js: test/smoke/coffee/%.coffee - @mkdir -p $(@D) - $(COFFEE) --compile -o $(@D) $< - - -public/js/ide.js: public/src/ide.js $(MODULE_IDE_SRC_FILES) +public/js/ide.js: public/src/ide.js $(IDE_SRC_FILES) @echo Compiling and injecting module includes into public/js/ide.js @INCLUDES=""; \ for dir in modules/*; \ @@ -92,7 +65,7 @@ public/js/ide.js: public/src/ide.js $(MODULE_IDE_SRC_FILES) sed -e s=\'__IDE_CLIENTSIDE_INCLUDES__\'=$$INCLUDES= \ > $@ -public/js/main.js: public/src/main.js $(MODULE_MAIN_SRC_FILES) +public/js/main.js: public/src/main.js $(MAIN_SRC_FILES) @echo Compiling and injecting module includes into public/js/main.js @INCLUDES=""; \ for dir in modules/*; \ @@ -114,7 +87,7 @@ css_full: $(CSS_FILES) css: $(CSS_OL_FILE) -minify: $(CSS_FILES) $(JS_FILES) $(OUTPUT_SRC_FILES) +minify: $(CSS_FILES) $(DIST_FILES) $(GRUNT) compile:minify $(MAKE) minify_css $(MAKE) minify_es @@ -128,18 +101,11 @@ minify_css: $(CSS_FILES) minify_es: npm -q run webpack:production -compile: compile_app $(OUTPUT_SRC_FILES) css public/js/main.js public/js/ide.js - -compile_app: $(JS_FILES) +compile: $(DIST_FILES) css public/js/main.js public/js/ide.js @$(MAKE) compile_modules compile_full: - $(COFFEE) -c -p app.coffee > app.js - $(COFFEE) -o app/js -c app/coffee $(BABEL) public/src --out-dir public/js - $(COFFEE) -o test/acceptance/js -c test/acceptance/coffee - $(COFFEE) -o test/smoke/js -c test/smoke/coffee - $(COFFEE) -o test/unit/js -c test/unit/coffee $(BABEL) test/unit_frontend/src --out-dir test/unit_frontend/js rm -f public/js/ide.js public/js/main.js # We need to generate ide.js, main.js manually later $(MAKE) css_full @@ -149,9 +115,6 @@ compile_full: compile_css_full: $(MAKE) css_full -compile_module: - cd modules/$(MODULE_NAME) && $(MAKE) compile - compile_modules: $(MODULE_MAKEFILES) @set -e; \ for dir in $(MODULE_DIRS); \ @@ -184,33 +147,18 @@ $(MODULE_MAKEFILES): Makefile.module cp Makefile.module $$makefile; \ done -clean: clean_app clean_frontend clean_css clean_tests clean_modules - -clean_app: - rm -f app.js app.js.map - rm -rf app/js +clean: clean_frontend clean_css clean_tests clean_frontend: rm -rf public/js/{analytics,directives,es,filters,ide,main,modules,services,utils} rm -f public/js/*.{js,map} -clean_tests: - rm -rf test/unit/js - rm -rf test/unit_frontend/js - rm -rf test/acceptance/js - -clean_modules: - for dir in modules/*; \ - do \ - rm -f $$dir/index.js; \ - rm -rf $$dir/app/js; \ - rm -rf $$dir/test/unit/js; \ - rm -rf $$dir/test/acceptance/js; \ - done - clean_css: rm -f public/stylesheets/*.css* +clean_tests: + rm -rf test/unit_frontend/js + clean_ci: $(DOCKER_COMPOSE) down -v -t 0 docker container list | grep 'days ago' | cut -d ' ' -f 1 - | xargs -r docker container stop @@ -219,12 +167,12 @@ clean_ci: test: test_unit test_frontend test_acceptance -test_module: compile_module test_unit_module_run test_acceptance_module_run +test_module: test_unit_module_run test_acceptance_module_run test_unit: @[ ! -d test/unit ] && echo "web has no unit tests" || COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit -test_unit_module: compile_module test_unit_module_run +test_unit_module: test_unit_module_run test_unit_module_run: COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit bin/unit_test_module $(MODULE_NAME) --grep=$(MOCHA_GREP) @@ -241,17 +189,17 @@ test_frontend_run: test_frontend_build_run: build_test_frontend test_frontend_run -test_acceptance: compile test_acceptance_app_run test_acceptance_modules_run +test_acceptance: test_acceptance_app_run test_acceptance_modules_run -test_acceptance_app: compile test_acceptance_app_run +test_acceptance_app: test_acceptance_app_run -test_acceptance_module: compile_module test_acceptance_module_run +test_acceptance_module: test_acceptance_module_run test_acceptance_run: test_acceptance_app_run test_acceptance_modules_run test_acceptance_app_run: COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 - COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_acceptance npm -q run test:acceptance:run_dir test/acceptance/js + COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_acceptance npm -q run test:acceptance:run_dir test/acceptance/src COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 test_acceptance_modules_run: diff --git a/services/web/Makefile.module b/services/web/Makefile.module index 29af9d297a..4a165c7e17 100644 --- a/services/web/Makefile.module +++ b/services/web/Makefile.module @@ -1,22 +1,16 @@ MODULE_NAME := $(notdir $(shell pwd)) MODULE_DIR := modules/$(MODULE_NAME) PROJECT_NAME = web -COFFEE := ../../node_modules/.bin/coffee BABEL := ../../node_modules/.bin/babel -APP_COFFEE_FILES := $(shell [ -e app/coffee ] && find app/coffee -name '*.coffee') \ - $(shell [ -e test/unit/coffee ] && find test/unit/coffee -name '*.coffee') \ - $(shell [ -e test/acceptance/coffee ] && find test/acceptance/coffee -name '*.coffee') -APP_JS_FILES := $(subst coffee,js,$(APP_COFFEE_FILES)) - IDE_SRC_FILES := $(shell [ -e public/src/ide ] && find public/src/ide -name '*.js') -IDE_OUTPUT_FILES := $(subst public/src/ide,../../public/js/ide/$(MODULE_NAME),$(IDE_SRC_FILES)) +IDE_DIST_FILES := $(subst public/src/ide,../../public/js/ide/$(MODULE_NAME),$(IDE_SRC_FILES)) IDE_TEST_SRC_FILES := $(shell [ -e test/unit_frontend/src/ide ] && find test/unit_frontend/src/ide -name '*.js') -IDE_TEST_OUTPUT_FILES := $(subst test/unit_frontend/src/ide,../../test/unit_frontend/js/ide/$(MODULE_NAME),$(IDE_TEST_SRC_FILES)) +IDE_TEST_DIST_FILES := $(subst test/unit_frontend/src/ide,../../test/unit_frontend/js/ide/$(MODULE_NAME),$(IDE_TEST_SRC_FILES)) MAIN_SRC_FILES := $(shell [ -e public/src/main ] && find public/src/main -name '*.js') -MAIN_OUTPUT_FILES := $(subst public/src/main,../../public/js/main/$(MODULE_NAME),$(MAIN_SRC_FILES)) +MAIN_DIST_FILES := $(subst public/src/main,../../public/js/main/$(MODULE_NAME),$(MAIN_SRC_FILES)) DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE_MODULE_FLAGS := ${DOCKER_COMPOSE_FLAGS} -f $(MODULE_DIR)/docker-compose.yml @@ -28,18 +22,6 @@ DOCKER_COMPOSE := cd ../../ && \ MOCHA_GREP=${MOCHA_GREP} \ docker-compose ${DOCKER_COMPOSE_MODULE_FLAGS} -app/js/%.js: app/coffee/%.coffee - @mkdir -p $(dir $@) - $(COFFEE) --compile --print $< > $@ - -test/unit/js/%.js: test/unit/coffee/%.coffee - @mkdir -p $(dir $@) - $(COFFEE) --compile --print $< > $@ - -test/acceptance/js/%.js: test/acceptance/coffee/%.coffee - @mkdir -p $(dir $@) - $(COFFEE) --compile --print $< > $@ - ../../test/unit_frontend/js/ide/$(MODULE_NAME)/%.js: test/unit_frontend/src/ide/%.js @mkdir -p $(dir $@) $(BABEL) $< --out-file $@ @@ -52,20 +34,14 @@ test/acceptance/js/%.js: test/acceptance/coffee/%.coffee @mkdir -p $(dir $@) $(BABEL) $< --out-file $@ -index.js: index.coffee - $(COFFEE) --compile --print $< > $@ - -compile: $(APP_JS_FILES) $(IDE_OUTPUT_FILES) $(MAIN_OUTPUT_FILES) $(IDE_TEST_OUTPUT_FILES) index.js +compile: $(IDE_DIST_FILES) $(MAIN_DIST_FILES) $(IDE_TEST_DIST_FILES) @echo > /dev/null compile_full: - if [ -e app/coffee ]; then $(COFFEE) -o app/js -c app/coffee; fi - if [ -e test/unit/coffee ]; then $(COFFEE) -o test/unit/js -c test/unit/coffee; fi - if [ -e test/acceptance/coffee ]; then $(COFFEE) -o test/acceptance/js -c test/acceptance/coffee; fi if [ -e public/src/ide ]; then $(BABEL) public/src/ide --out-dir ../../public/js/ide/$(MODULE_NAME); fi if [ -e public/src/main ]; then $(BABEL) public/src/main --out-dir ../../public/js/main/$(MODULE_NAME); fi if [ -e test/unit_frontend/src/ide ]; then $(BABEL) test/unit_frontend/src/ide --out-dir ../../test/unit_frontend/js/ide/$(MODULE_NAME); fi - @$(MAKE) compile + @$(MAKE) compile # Anything else missed test_acceptance: - ${DOCKER_COMPOSE} run --rm test_acceptance npm -q run test:acceptance:run_dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/js + ${DOCKER_COMPOSE} run --rm test_acceptance npm -q run test:acceptance:run_dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/src diff --git a/services/web/app.coffee b/services/web/app.coffee deleted file mode 100644 index e9033f8d1b..0000000000 --- a/services/web/app.coffee +++ /dev/null @@ -1,41 +0,0 @@ -metrics = require("metrics-sharelatex") -metrics.initialize(process.env['METRICS_APP_NAME'] or "web") -Settings = require('settings-sharelatex') -logger = require 'logger-sharelatex' -logger.initialize(process.env['METRICS_APP_NAME'] or "web") -logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user -logger.logger.serializers.docs = require("./app/js/infrastructure/LoggerSerializers").docs -logger.logger.serializers.files = require("./app/js/infrastructure/LoggerSerializers").files -logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project -if Settings.sentry?.dsn? - logger.initializeErrorReporting(Settings.sentry.dsn) - -metrics.memory.monitor(logger) -Server = require("./app/js/infrastructure/Server") - -argv = require("optimist") - .options("user", {alias : "u", description : "Run the server with permissions of the specified user"}) - .options("group", {alias : "g", description : "Run the server with permissions of the specified group"}) - .usage("Usage: $0") - .argv - -if Settings.catchErrors - process.removeAllListeners "uncaughtException" - process.on "uncaughtException", (error) -> - logger.error err: error, "uncaughtException" - -port = Settings.port or Settings.internal?.web?.port or 3000 -host = Settings.internal.web.host or "localhost" -if !module.parent # Called directly - Server.server.listen port, host, -> - logger.info "web starting up, listening on #{host}:#{port}" - logger.info("#{require('http').globalAgent.maxSockets} sockets enabled") - if argv.user - process.setuid argv.user - logger.info "Running as user: #{argv.user}" - if argv.group - process.setgid argv.group - logger.info "Running as group: #{argv.group}" - -module.exports = Server.server - diff --git a/services/web/app.js b/services/web/app.js new file mode 100644 index 0000000000..2b92330cd9 --- /dev/null +++ b/services/web/app.js @@ -0,0 +1,77 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const metrics = require('metrics-sharelatex') +metrics.initialize(process.env['METRICS_APP_NAME'] || 'web') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +logger.initialize(process.env['METRICS_APP_NAME'] || 'web') +logger.logger.serializers.user = require('./app/src/infrastructure/LoggerSerializers').user +logger.logger.serializers.docs = require('./app/src/infrastructure/LoggerSerializers').docs +logger.logger.serializers.files = require('./app/src/infrastructure/LoggerSerializers').files +logger.logger.serializers.project = require('./app/src/infrastructure/LoggerSerializers').project +if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { + logger.initializeErrorReporting(Settings.sentry.dsn) +} + +metrics.memory.monitor(logger) +const Server = require('./app/src/infrastructure/Server') + +const { argv } = require('optimist') + .options('user', { + alias: 'u', + description: 'Run the server with permissions of the specified user' + }) + .options('group', { + alias: 'g', + description: 'Run the server with permissions of the specified group' + }) + .usage('Usage: $0') + +if (Settings.catchErrors) { + process.removeAllListeners('uncaughtException') + process.on('uncaughtException', error => + logger.error({ err: error }, 'uncaughtException') + ) +} + +const port = + Settings.port || + __guard__( + Settings.internal != null ? Settings.internal.web : undefined, + x => x.port + ) || + 3000 +const host = Settings.internal.web.host || 'localhost' +if (!module.parent) { + // Called directly + Server.server.listen(port, host, function() { + logger.info(`web starting up, listening on ${host}:${port}`) + logger.info(`${require('http').globalAgent.maxSockets} sockets enabled`) + if (argv.user) { + process.setuid(argv.user) + logger.info(`Running as user: ${argv.user}`) + } + if (argv.group) { + process.setgid(argv.group) + return logger.info(`Running as group: ${argv.group}`) + } + }) +} + +module.exports = Server.server + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee deleted file mode 100644 index e407feb488..0000000000 --- a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee +++ /dev/null @@ -1,42 +0,0 @@ -AnalyticsManager = require "./AnalyticsManager" -Errors = require "../Errors/Errors" -AuthenticationController = require("../Authentication/AuthenticationController") -InstitutionsAPI = require("../Institutions/InstitutionsAPI") -GeoIpLookup = require '../../infrastructure/GeoIpLookup' - -module.exports = AnalyticsController = - updateEditingSession: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - projectId = req.params.projectId - countryCode = null - - if userId? - GeoIpLookup.getDetails req.ip, (err, geoDetails) -> - if geoDetails?.country_code? and geoDetails.country_code != "" - countryCode = geoDetails.country_code - AnalyticsManager.updateEditingSession userId, projectId, countryCode, (error) -> - respondWith(error, res, next) - else - res.send 204 - - recordEvent: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) or req.sessionID - AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) -> - respondWith(error, res, next) - - licences: (req, res, next) -> - {resource_id, start_date, end_date, lag} = req.query - InstitutionsAPI.getInstitutionLicences resource_id, start_date, end_date, lag, (error, licences) -> - if error? - next(error) - else - res.send licences - -respondWith = (error, res, next) -> - if error instanceof Errors.ServiceNotConfiguredError - # ignore, no-op - res.send(204) - else if error? - next(error) - else - res.send 204 diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee deleted file mode 100644 index ebb985f7b5..0000000000 --- a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee +++ /dev/null @@ -1,109 +0,0 @@ -settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -_ = require "underscore" -request = require "requestretry" -Errors = require '../Errors/Errors' - - -makeFaultTolerantRequest = (userId, options, callback) -> - if userId+"" == settings.smokeTest?.userId+"" - return callback() - - options = Object.assign(options, { - delayStrategy: exponentialBackoffStrategy() - timeout: 30000 - }) - - if settings.overleaf? - options.qs = Object.assign({}, options.qs, { fromV2: 1 }) - - makeRequest options, (err) -> - if err? - logger.err { err: err }, 'Request to analytics failed' - - callback() # Do not wait for all the attempts - -makeRequest = (opts, callback)-> - if settings.apis?.analytics?.url? - urlPath = opts.url - opts.url = "#{settings.apis.analytics.url}#{urlPath}" - request opts, callback - else - callback(new Errors.ServiceNotConfiguredError('Analytics service not configured')) - - -# Set an exponential backoff to retry calls to analytics. First retry will -# happen after 4s, then 8, 16, 32, 64... -exponentialBackoffStrategy = () -> - attempts = 1 # This won't be called until there has been 1 failure - - () -> - attempts += 1 - exponentialBackoffDelay(attempts) - -exponentialBackoffDelay = (attempts) -> - delay = 2 ** attempts * 1000 - - logger.warn "Error comunicating with the analytics service. " + - "Will try again attempt #{attempts} in #{delay}ms" - - delay - - -module.exports = - - identifyUser: (user_id, old_user_id, callback = (error)->)-> - opts = - body: - old_user_id:old_user_id - json:true - method:"POST" - timeout:1000 - url: "/user/#{user_id}/identify" - makeRequest opts, callback - - recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) -> - opts = - body: - event:event - segmentation:segmentation - json:true - method:"POST" - url: "/user/#{user_id}/event" - maxAttempts: 7 # Give up after ~ 8min - - makeFaultTolerantRequest user_id, opts, callback - - - updateEditingSession: (userId, projectId, countryCode, callback = (error) ->) -> - query = - userId: userId - projectId: projectId - - if countryCode - query.countryCode = countryCode - - opts = - method: "PUT" - url: "/editingSession" - qs: query - maxAttempts: 6 # Give up after ~ 4min - - makeFaultTolerantRequest userId, opts, callback - - - getLastOccurrence: (user_id, event, callback = (error) ->) -> - opts = - body: - event:event - json:true - method:"POST" - timeout:1000 - url: "/user/#{user_id}/event/last_occurrence" - makeRequest opts, (err, response, body)-> - if err? - console.log response, opts - logger.err {user_id, err}, "error getting last occurance of event" - return callback err - else - return callback null, body diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsProxy.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsProxy.coffee deleted file mode 100644 index f513db8414..0000000000 --- a/services/web/app/coffee/Features/Analytics/AnalyticsProxy.coffee +++ /dev/null @@ -1,20 +0,0 @@ -settings = require "settings-sharelatex" -Errors = require '../Errors/Errors' -httpProxy = require 'express-http-proxy' -URL = require 'url' - -module.exports = - call: (basePath) -> - analyticsUrl = settings?.apis?.analytics?.url - if analyticsUrl? - httpProxy(analyticsUrl, - proxyReqPathResolver: (req) -> - requestPath = URL.parse(req.url).path - "#{basePath}#{requestPath}" - proxyReqOptDecorator: (proxyReqOpts, srcReq) -> - proxyReqOpts.headers = {} # unset all headers - proxyReqOpts - ) - else - (req, res, next) -> - next(new Errors.ServiceNotConfiguredError('Analytics service not configured')) diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee deleted file mode 100644 index 57b131326f..0000000000 --- a/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee +++ /dev/null @@ -1,26 +0,0 @@ -AuthenticationController = require './../Authentication/AuthenticationController' -AnalyticsController = require('./AnalyticsController') -AnalyticsProxy = require('./AnalyticsProxy') - -module.exports = - apply: (webRouter, privateApiRouter, publicApiRouter) -> - webRouter.post '/event/:event', AnalyticsController.recordEvent - - webRouter.put '/editingSession/:projectId', - AnalyticsController.updateEditingSession - - publicApiRouter.use '/analytics/graphs', - AuthenticationController.httpAuth, - AnalyticsProxy.call('/graphs') - - publicApiRouter.use '/analytics/recentTeamActivity', - AuthenticationController.httpAuth, - AnalyticsProxy.call('/recentTeamActivity') - - publicApiRouter.use '/analytics/recentV1TemplateIdsActivity', - AuthenticationController.httpAuth, - AnalyticsProxy.call('/recentV1TemplateIdsActivity') - - publicApiRouter.use '/analytics/uniExternalCollaboration', - AuthenticationController.httpAuth, - AnalyticsProxy.call('/uniExternalCollaboration') diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee deleted file mode 100644 index 9c3a9f4deb..0000000000 --- a/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee +++ /dev/null @@ -1,24 +0,0 @@ -AnnouncementsHandler = require("./AnnouncementsHandler") -AuthenticationController = require("../Authentication/AuthenticationController") -logger = require("logger-sharelatex") -settings = require("settings-sharelatex") - -module.exports = - - getUndreadAnnouncements: (req, res, next)-> - if !settings?.apis?.analytics?.url? or !settings.apis.blog.url? - return res.json [] - - user = AuthenticationController.getSessionUser(req) - logger.log {user_id:user?._id}, "getting unread announcements" - AnnouncementsHandler.getUnreadAnnouncements user, (err, announcements)-> - if err? - logger.err {err:err, user_id:user._id}, "unable to get unread announcements" - next(err) - else - res.json announcements - - - - - diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee deleted file mode 100644 index ec3e92e980..0000000000 --- a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee +++ /dev/null @@ -1,67 +0,0 @@ -AnalyticsManager = require("../Analytics/AnalyticsManager") -BlogHandler = require("../Blog/BlogHandler") -logger = require("logger-sharelatex") -settings = require("settings-sharelatex") -async = require("async") -_ = require("lodash") - -module.exports = AnnouncementsHandler = - - _domainSpecificAnnouncements : (email)-> - domainSpecific = _.filter settings?.domainAnnouncements, (domainAnnouncment)-> - matches = _.filter domainAnnouncment.domains, (domain)-> - return email.indexOf(domain) != -1 - return matches.length > 0 and domainAnnouncment.id? - return domainSpecific or [] - - - getUnreadAnnouncements : (user, callback = (err, announcements)->)-> - if !user? and !user._id? - return callback("user not supplied") - - timestamp = user._id.toString().substring(0,8) - userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 ) - - async.parallel { - lastEvent: (cb)-> - AnalyticsManager.getLastOccurrence user._id, "announcement-alert-dismissed", cb - announcements: (cb)-> - BlogHandler.getLatestAnnouncements cb - }, (err, results)-> - if err? - logger.err err:err, user_id:user._id, "error getting unread announcements" - return callback(err) - - domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(user?.email) - - domainSpecific = _.map domainSpecific, (domainAnnouncment)-> - try - domainAnnouncment.date = new Date(domainAnnouncment.date) - return domainAnnouncment - catch e - return callback(e) - - announcements = results.announcements - announcements = _.union announcements, domainSpecific - announcements = _.sortBy(announcements, "date").reverse() - - lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId - - announcementIndex = _.findIndex announcements, (announcement)-> - announcement.id == lastSeenBlogId - - announcements = _.map announcements, (announcement, index)-> - if announcement.date < userSignupDate - read = true - else if announcementIndex == -1 - read = false - else if index >= announcementIndex - read = true - else - read = false - announcement.read = read - return announcement - - logger.log announcementsLength:announcements?.length, user_id:user?._id, "returning announcements" - - callback null, announcements diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee deleted file mode 100644 index 408ae43400..0000000000 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ /dev/null @@ -1,321 +0,0 @@ -AuthenticationManager = require ("./AuthenticationManager") -LoginRateLimiter = require("../Security/LoginRateLimiter") -UserUpdater = require "../User/UserUpdater" -Metrics = require('metrics-sharelatex') -logger = require("logger-sharelatex") -querystring = require('querystring') -Url = require("url") -Settings = require "settings-sharelatex" -basicAuth = require('basic-auth-connect') -UserHandler = require("../User/UserHandler") -UserSessionsManager = require("../User/UserSessionsManager") -Analytics = require "../Analytics/AnalyticsManager" -passport = require 'passport' -NotificationsBuilder = require("../Notifications/NotificationsBuilder") -SudoModeHandler = require '../SudoMode/SudoModeHandler' -V1Api = require "../V1/V1Api" -{User} = require "../../models/User" -{ URL } = require('url') - -module.exports = AuthenticationController = - - serializeUser: (user, callback) -> - lightUser = - _id: user._id - first_name: user.first_name - last_name: user.last_name - isAdmin: user.isAdmin - staffAccess: user.staffAccess - email: user.email - referal_id: user.referal_id - session_created: (new Date()).toISOString() - ip_address: user._login_req_ip - must_reconfirm: user.must_reconfirm - v1_id: user.overleaf?.id - callback(null, lightUser) - - deserializeUser: (user, cb) -> - cb(null, user) - - afterLoginSessionSetup: (req, user, callback=(err)->) -> - req.login user, (err) -> - if err? - logger.err {user_id: user._id, err}, "error from req.login" - return callback(err) - # Regenerate the session to get a new sessionID (cookie value) to - # protect against session fixation attacks - oldSession = req.session - req.session.destroy (err) -> - if err? - logger.err {user_id: user._id, err}, "error when trying to destroy old session" - return callback(err) - req.sessionStore.generate(req) - for key, value of oldSession - req.session[key] = value unless key == '__tmp' - # copy to the old `session.user` location, for backward-comptability - req.session.user = req.session.passport.user - req.session.save (err) -> - if err? - logger.err {user_id: user._id}, "error saving regenerated session after login" - return callback(err) - UserSessionsManager.trackSession(user, req.sessionID, () ->) - callback(null) - - passportLogin: (req, res, next) -> - # This function is middleware which wraps the passport.authenticate middleware, - # so we can send back our custom `{message: {text: "", type: ""}}` responses on failure, - # and send a `{redir: ""}` response on success - passport.authenticate('local', (err, user, info) -> - if err? - return next(err) - if user # `user` is either a user object or false - AuthenticationController.finishLogin(user, req, res, next) - else - if info.redir? - res.json {redir: info.redir} - else - res.json message: info - )(req, res, next) - - finishLogin: (user, req, res, next) -> - return res.redirect('/login') if user == false # OAuth2 'state' mismatch - if user.must_reconfirm - AuthenticationController._redirectToReconfirmPage req, res, user - else - redir = AuthenticationController._getRedirectFromSession(req) || "/project" - AuthenticationController._loginAsyncHandlers(req, user) - AuthenticationController.afterLoginSessionSetup req, user, (err) -> - if err? - return next(err) - SudoModeHandler.activateSudoMode user._id, (err) -> - if err? - logger.err {err, user_id: user._id}, "Error activating Sudo Mode on login, continuing" - AuthenticationController._clearRedirectFromSession(req) - if req.headers?['accept']?.match(/^application\/json.*$/) - res.json {redir: redir} - else - res.redirect(redir) - - doPassportLogin: (req, username, password, done) -> - email = username.toLowerCase() - Modules = require "../../infrastructure/Modules" - Modules.hooks.fire 'preDoPassportLogin', req, email, (err, infoList) -> - return next(err) if err? - info = infoList.find((i) => i?) - if info? - return done(null, false, info) - LoginRateLimiter.processLoginRequest email, (err, isAllowed)-> - return done(err) if err? - if !isAllowed - logger.log email:email, "too many login requests" - return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'}) - AuthenticationManager.authenticate email: email, password, (error, user) -> - return done(error) if error? - if user? - # async actions - return done(null, user) - else - AuthenticationController._recordFailedLogin() - logger.log email: email, "failed log in" - return done( - null, - false, - {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'} - ) - - _loginAsyncHandlers: (req, user) -> - UserHandler.setupLoginData(user, ()->) - LoginRateLimiter.recordSuccessfulLogin(user.email) - AuthenticationController._recordSuccessfulLogin(user._id) - AuthenticationController.ipMatchCheck(req, user) - Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip}) - Analytics.identifyUser(user._id, req.sessionID) - logger.log email: user.email, user_id: user._id.toString(), "successful log in" - req.session.justLoggedIn = true - # capture the request ip for use when creating the session - user._login_req_ip = req.ip - - ipMatchCheck: (req, user) -> - if req.ip != user.lastLoginIp - NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip) - UserUpdater.updateUser user._id.toString(), { - $set: { "lastLoginIp": req.ip } - } - - setInSessionUser: (req, props) -> - for key, value of props - if req?.session?.passport?.user? - req.session.passport.user[key] = value - if req?.session?.user? - req.session.user[key] = value - - isUserLoggedIn: (req) -> - user_id = AuthenticationController.getLoggedInUserId(req) - return (user_id not in [null, undefined, false]) - - # TODO: perhaps should produce an error if the current user is not present - getLoggedInUserId: (req) -> - user = AuthenticationController.getSessionUser(req) - if user - return user._id - else - return null - - getLoggedInUserV1Id: (req) -> - user = AuthenticationController.getSessionUser(req) - if user?.v1_id? - return user.v1_id - else - return null - - getSessionUser: (req) -> - if req?.session?.user? - return req.session.user - else if req?.session?.passport?.user - return req.session.passport.user - else - return null - - requireLogin: () -> - doRequest = (req, res, next = (error) ->) -> - if !AuthenticationController.isUserLoggedIn(req) - AuthenticationController._redirectToLoginOrRegisterPage(req, res) - else - req.user = AuthenticationController.getSessionUser(req) - next() - - return doRequest - - # access tokens might be associated with user stubs if the user is - # not yet migrated to v2. if api can work with user stubs then set - # allowUserStub true when adding middleware to route. - requireOauth: (allowUserStub=false) -> - # require this here because module may not be included in some versions - Oauth2Server = require "../../../../modules/oauth2-server/app/js/Oauth2Server" - return (req, res, next = (error) ->) -> - request = new Oauth2Server.Request(req) - response = new Oauth2Server.Response(res) - Oauth2Server.server.authenticate request, response, {}, (err, token) -> - if err? - # use a 401 status code for malformed header for git-bridge - err.code = 401 if err.code == 400 and err.message == 'Invalid request: malformed authorization header' - # fall back to v1 on invalid token - return AuthenticationController._requireOauthV1Fallback req, res, next if err.code == 401 - # send all other errors - return res.status(err.code).json({error: err.name, error_description: err.message}) - return res.sendStatus 401 if token.user.constructor.modelName == "UserStub" and !allowUserStub - req.oauth = - access_token: token.accessToken - req.oauth_token = token - req.oauth_user = token.user - return next() - - _requireOauthV1Fallback: (req, res, next) -> - return res.sendStatus 401 unless req.token? - options = - expectedStatusCodes: [401] - json: token: req.token - method: "POST" - uri: "/api/v1/sharelatex/oauth_authorize" - V1Api.request options, (error, response, body) -> - return next(error) if error? - return res.status(401).json({error: "invalid_token"}) unless body?.user_profile?.id - User.findOne { "overleaf.id": body.user_profile.id }, (error, user) -> - return next(error) if error? - return res.status(401).json({error: "invalid_token"}) unless user? - req.oauth = - access_token: body.access_token - req.oauth_user = user - next() - - _globalLoginWhitelist: [] - addEndpointToLoginWhitelist: (endpoint) -> - AuthenticationController._globalLoginWhitelist.push endpoint - - requireGlobalLogin: (req, res, next) -> - if req._parsedUrl.pathname in AuthenticationController._globalLoginWhitelist - return next() - - if req.headers['authorization']? - return AuthenticationController.httpAuth(req, res, next) - else if AuthenticationController.isUserLoggedIn(req) - return next() - else - logger.log url:req.url, "user trying to access endpoint not in global whitelist" - AuthenticationController.setRedirectInSession(req) - return res.redirect "/login" - - httpAuth: basicAuth (user, pass)-> - isValid = Settings.httpAuthUsers[user] == pass - if !isValid - logger.err user:user, pass:pass, "invalid login details" - return isValid - - setRedirectInSession: (req, value) -> - if !value? - value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}" - if ( - req.session? && - !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) && - !/^.*\.(png|jpeg|svg)$/.test(value) - ) - safePath = AuthenticationController._getSafeRedirectPath(value) - req.session.postLoginRedirect = safePath - - _redirectToLoginOrRegisterPage: (req, res)-> - if (req.query.zipUrl? or req.query.project_name? or req.path == '/user/subscription/new') - return AuthenticationController._redirectToRegisterPage(req, res) - else - AuthenticationController._redirectToLoginPage(req, res) - - _redirectToLoginPage: (req, res) -> - logger.log url: req.url, "user not logged in so redirecting to login page" - AuthenticationController.setRedirectInSession(req) - url = "/login?#{querystring.stringify(req.query)}" - res.redirect url - Metrics.inc "security.login-redirect" - - _redirectToReconfirmPage: (req, res, user) -> - logger.log url: req.url, "user needs to reconfirm so redirecting to reconfirm page" - req.session.reconfirm_email = user?.email - redir = "/user/reconfirm" - if req.headers?['accept']?.match(/^application\/json.*$/) - res.json {redir: redir} - else - res.redirect redir - - _redirectToRegisterPage: (req, res) -> - logger.log url: req.url, "user not logged in so redirecting to register page" - AuthenticationController.setRedirectInSession(req) - url = "/register?#{querystring.stringify(req.query)}" - res.redirect url - Metrics.inc "security.login-redirect" - - _recordSuccessfulLogin: (user_id, callback = (error) ->) -> - UserUpdater.updateUser user_id.toString(), { - $set: { "lastLoggedIn": new Date() }, - $inc: { "loginCount": 1 } - }, (error) -> - callback(error) if error? - Metrics.inc "user.login.success" - callback() - - _recordFailedLogin: (callback = (error) ->) -> - Metrics.inc "user.login.failed" - callback() - - _getRedirectFromSession: (req) -> - value = req?.session?.postLoginRedirect - safePath = AuthenticationController._getSafeRedirectPath(value) if value - return safePath || null - - _clearRedirectFromSession: (req) -> - if req.session? - delete req.session.postLoginRedirect - - _getSafeRedirectPath: (value) -> - baseURL = Settings.siteUrl # base URL is required to construct URL from path - url = new URL(value, baseURL) - safePath = "#{url.pathname}#{url.search}#{url.hash}" - safePath = undefined if safePath == '/' - safePath diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee deleted file mode 100644 index 317fe00e73..0000000000 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ /dev/null @@ -1,136 +0,0 @@ -Settings = require "settings-sharelatex" -User = require("../../models/User").User -{db, ObjectId} = require("../../infrastructure/mongojs") -crypto = require 'crypto' -bcrypt = require 'bcrypt' -EmailHelper = require("../Helpers/EmailHelper") -Errors = require("../Errors/Errors") -UserGetter = require("../User/UserGetter") -V1Handler = require '../V1/V1Handler' - -BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12 - -_checkWriteResult = (result, callback = (error, updated) ->) -> - # for MongoDB - if result and result.nModified == 1 - callback(null, true) - else - callback(null, false) - -module.exports = AuthenticationManager = - authenticate: (query, password, callback = (error, user) ->) -> - # Using Mongoose for legacy reasons here. The returned User instance - # gets serialized into the session and there may be subtle differences - # between the user returned by Mongoose vs mongojs (such as default values) - User.findOne query, (error, user) => - return callback(error) if error? - if user? - if user.hashedPassword? - 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 - else - callback null, null - else - callback null, null - - validateEmail: (email) -> - parsed = EmailHelper.parseEmail(email) - if !parsed? - return { message: 'email not valid' } - return null - - # validates a password based on a similar set of rules to `complexPassword.js` on the frontend - # note that `passfield.js` enforces more rules than this, but these are the most commonly set. - # returns null on success, or an error string. - validatePassword: (password) -> - return { message: 'password not set' } unless password? - - allowAnyChars = Settings.passwordStrengthOptions?.allowAnyChars == true - min = Settings.passwordStrengthOptions?.length?.min || 6 - max = Settings.passwordStrengthOptions?.length?.max || 72 - - # we don't support passwords > 72 characters in length, because bcrypt truncates them - max = 72 if max > 72 - - return { message: 'password is too short' } unless password.length >= min - return { message: 'password is too long' } unless password.length <= max - return { message: 'password contains an invalid character' } unless allowAnyChars || AuthenticationManager._passwordCharactersAreValid(password) - return null - - setUserPassword: (user_id, password, callback = (error, changed) ->) -> - validation = @validatePassword(password) - return callback(validation.message) if validation? - - UserGetter.getUser user_id, { email:1, overleaf: 1 }, (error, user) -> - return callback(error) if error? - v1IdExists = user.overleaf?.id? - if v1IdExists and Settings.overleaf? # v2 user in v2 - # v2 user in v2, change password in v1 - AuthenticationManager.setUserPasswordInV1(user.overleaf.id, password, callback) - else if v1IdExists and !Settings.overleaf? - # v2 user in SL - return callback(new Errors.NotInV2Error("Password Reset Attempt")) - else if !v1IdExists and !Settings.overleaf? - # SL user in SL, change password in SL - AuthenticationManager.setUserPasswordInV2(user_id, password, callback) - else if !v1IdExists and Settings.overleaf? - # SL user in v2, should not happen - return callback(new Errors.SLInV2Error("Password Reset Attempt")) - else - return callback(new Error("Password Reset Attempt Failed")) - - checkRounds: (user, hashedPassword, password, callback = (error) ->) -> - # Temporarily disable this function, TODO: re-enable this - if Settings?.security?.disableBcryptRoundsUpgrades - return callback() - # 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() - - setUserPasswordInV2: (user_id, password, callback) -> - validation = @validatePassword(password) - return callback(validation.message) if validation? - minorVersion = 'a' - bcrypt.genSalt BCRYPT_ROUNDS, minorVersion, (error, salt) -> - return callback(error) if error? - bcrypt.hash password, salt, (error, hash) -> - return callback(error) if error? - db.users.update({ - _id: ObjectId(user_id.toString()) - }, { - $set: hashedPassword: hash - $unset: password: true - }, (updateError, result)-> - return callback(updateError) if updateError? - _checkWriteResult(result, callback) - ) - - setUserPasswordInV1: (v1_user_id, password, callback) -> - validation = @validatePassword(password) - return callback(validation.message) if validation? - - V1Handler.doPasswordReset v1_user_id, password, (error, reset)-> - return callback(error) if error? - return callback(error, reset) - - _passwordCharactersAreValid: (password) -> - digits = Settings.passwordStrengthOptions?.chars?.digits || '1234567890' - letters = Settings.passwordStrengthOptions?.chars?.letters || 'abcdefghijklmnopqrstuvwxyz' - letters_up = Settings.passwordStrengthOptions?.chars?.letters_up || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - symbols = Settings.passwordStrengthOptions?.chars?.symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,' - - for charIndex in [0..password.length - 1] - return false unless digits.indexOf(password[charIndex]) > -1 or - letters.indexOf(password[charIndex]) > -1 or - letters_up.indexOf(password[charIndex]) > -1 or - symbols.indexOf(password[charIndex]) > -1 - return true diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee deleted file mode 100644 index 0cebb45fb1..0000000000 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ /dev/null @@ -1,118 +0,0 @@ -CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") -ProjectGetter = require('../Project/ProjectGetter') -User = require("../../models/User").User -PrivilegeLevels = require("./PrivilegeLevels") -PublicAccessLevels = require("./PublicAccessLevels") -Errors = require("../Errors/Errors") -ObjectId = require("mongojs").ObjectId -TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') - - -module.exports = AuthorizationManager = - - getPublicAccessLevel: (project_id, callback=(err, level)->) -> - if !ObjectId.isValid(project_id) - return callback(new Error("invalid project id")) - # Note, the Project property in the DB is `publicAccesLevel`, without the second `s` - ProjectGetter.getProject project_id, publicAccesLevel: 1, (error, project) -> - return callback(error) if error? - if !project? - return callback new Errors.NotFoundError("no project found with id #{project_id}") - callback null, project.publicAccesLevel - - # Get the privilege level that the user has for the project - # Returns: - # * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has - # access. false if the user does not have access - # * becausePublic: true if the access level is only because the project is public. - # * becauseSiteAdmin: true if access level is only because user is admin - getPrivilegeLevelForProject: ( - user_id, project_id, token, - callback = (error, privilegeLevel, becausePublic, becauseSiteAdmin) -> - ) -> - if !user_id? - # User is Anonymous, Try Token-based access - AuthorizationManager.getPublicAccessLevel project_id, (err, publicAccessLevel) -> - return callback(err) if err? - if publicAccessLevel == PublicAccessLevels.TOKEN_BASED - # Anonymous users can have read-only access to token-based projects, - # while read-write access must be logged in, - # unless the `enableAnonymousReadAndWriteSharing` setting is enabled - TokenAccessHandler.isValidToken project_id, token, (err, isValidReadAndWrite, isValidReadOnly) -> - return callback(err) if err? - if isValidReadOnly - # Grant anonymous user read-only access - callback null, PrivilegeLevels.READ_ONLY, false, false - else if ( - isValidReadAndWrite and - TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED - ) - # Grant anonymous user read-and-write access - callback null, PrivilegeLevels.READ_AND_WRITE, false, false - else - # Deny anonymous access - callback null, PrivilegeLevels.NONE, false, false - else if publicAccessLevel == PublicAccessLevels.READ_ONLY - # Legacy public read-only access for anonymous user - callback null, PrivilegeLevels.READ_ONLY, true, false - else if publicAccessLevel == PublicAccessLevels.READ_AND_WRITE - # Legacy public read-write access for anonymous user - callback null, PrivilegeLevels.READ_AND_WRITE, true, false - else - # Deny anonymous user access - callback null, PrivilegeLevels.NONE, false, false - else - # User is present, get their privilege level from database - CollaboratorsHandler.getMemberIdPrivilegeLevel user_id, project_id, (error, privilegeLevel) -> - return callback(error) if error? - if privilegeLevel? and privilegeLevel != PrivilegeLevels.NONE - # The user has direct access - callback null, privilegeLevel, false, false - else - AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) -> - return callback(error) if error? - if isAdmin - callback null, PrivilegeLevels.OWNER, false, true - else - # Legacy public-access system - # User is present (not anonymous), but does not have direct access - AuthorizationManager.getPublicAccessLevel project_id, (err, publicAccessLevel) -> - return callback(err) if err? - if publicAccessLevel == PublicAccessLevels.READ_ONLY - callback null, PrivilegeLevels.READ_ONLY, true, false - else if publicAccessLevel == PublicAccessLevels.READ_AND_WRITE - callback null, PrivilegeLevels.READ_AND_WRITE, true, false - else - callback null, PrivilegeLevels.NONE, false, false - - canUserReadProject: (user_id, project_id, token, callback = (error, canRead) ->) -> - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) -> - return callback(error) if error? - return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE, PrivilegeLevels.READ_ONLY]) - - canUserWriteProjectContent: (user_id, project_id, token, callback = (error, canWriteContent) ->) -> - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) -> - return callback(error) if error? - return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE]) - - canUserWriteProjectSettings: (user_id, project_id, token, callback = (error, canWriteSettings) ->) -> - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel, becausePublic) -> - return callback(error) if error? - if privilegeLevel == PrivilegeLevels.OWNER - return callback null, true - else if privilegeLevel == PrivilegeLevels.READ_AND_WRITE and !becausePublic - return callback null, true - else - return callback null, false - - canUserAdminProject: (user_id, project_id, token, callback = (error, canAdmin, becauseSiteAdmin) ->) -> - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel, becausePublic, becauseSiteAdmin) -> - return callback(error) if error? - return callback null, (privilegeLevel == PrivilegeLevels.OWNER), becauseSiteAdmin - - isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> - if !user_id? - return callback null, false - User.findOne { _id: user_id }, { isAdmin: 1 }, (error, user) -> - return callback(error) if error? - return callback null, (user?.isAdmin == true) diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddleware.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddleware.coffee deleted file mode 100644 index e3b913cb91..0000000000 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddleware.coffee +++ /dev/null @@ -1,122 +0,0 @@ -AuthorizationManager = require("./AuthorizationManager") -async = require "async" -logger = require "logger-sharelatex" -ObjectId = require("mongojs").ObjectId -Errors = require "../Errors/Errors" -AuthenticationController = require "../Authentication/AuthenticationController" -TokenAccessHandler = require '../TokenAccess/TokenAccessHandler' - -module.exports = AuthorizationMiddleware = - ensureUserCanReadMultipleProjects: (req, res, next) -> - project_ids = (req.query.project_ids or "").split(",") - AuthorizationMiddleware._getUserId req, (error, user_id) -> - return next(error) if error? - # Remove the projects we have access to. Note rejectSeries doesn't use - # errors in callbacks - async.rejectSeries project_ids, (project_id, cb) -> - token = TokenAccessHandler.getRequestToken(req, project_id) - AuthorizationManager.canUserReadProject user_id, project_id, token, (error, canRead) -> - return next(error) if error? - cb(canRead) - , (unauthorized_project_ids) -> - if unauthorized_project_ids.length > 0 - AuthorizationMiddleware.redirectToRestricted req, res, next - else - next() - - ensureUserCanReadProject: (req, res, next) -> - AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) -> - return next(error) if error? - token = TokenAccessHandler.getRequestToken(req, project_id) - AuthorizationManager.canUserReadProject user_id, project_id, token, (error, canRead) -> - return next(error) if error? - if canRead - logger.log {user_id, project_id}, "allowing user read access to project" - next() - else - logger.log {user_id, project_id}, "denying user read access to project" - if req.headers?['accept']?.match(/^application\/json.*$/) - res.sendStatus(403) - else - AuthorizationMiddleware.redirectToRestricted req, res, next - - ensureUserCanWriteProjectSettings: (req, res, next) -> - AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) -> - return next(error) if error? - token = TokenAccessHandler.getRequestToken(req, project_id) - AuthorizationManager.canUserWriteProjectSettings user_id, project_id, token, (error, canWrite) -> - return next(error) if error? - if canWrite - logger.log {user_id, project_id}, "allowing user write access to project settings" - next() - else - logger.log {user_id, project_id}, "denying user write access to project settings" - AuthorizationMiddleware.redirectToRestricted req, res, next - - ensureUserCanWriteProjectContent: (req, res, next) -> - AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) -> - return next(error) if error? - token = TokenAccessHandler.getRequestToken(req, project_id) - AuthorizationManager.canUserWriteProjectContent user_id, project_id, token, (error, canWrite) -> - return next(error) if error? - if canWrite - logger.log {user_id, project_id}, "allowing user write access to project content" - next() - else - logger.log {user_id, project_id}, "denying user write access to project settings" - AuthorizationMiddleware.redirectToRestricted req, res, next - - ensureUserCanAdminProject: (req, res, next) -> - AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) -> - return next(error) if error? - token = TokenAccessHandler.getRequestToken(req, project_id) - AuthorizationManager.canUserAdminProject user_id, project_id, token, (error, canAdmin) -> - return next(error) if error? - if canAdmin - logger.log {user_id, project_id}, "allowing user admin access to project" - next() - else - logger.log {user_id, project_id}, "denying user admin access to project" - AuthorizationMiddleware.redirectToRestricted req, res, next - - ensureUserIsSiteAdmin: (req, res, next) -> - AuthorizationMiddleware._getUserId req, (error, user_id) -> - return next(error) if error? - AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) -> - return next(error) if error? - if isAdmin - logger.log {user_id}, "allowing user admin access to site" - next() - else - logger.log {user_id}, "denying user admin access to site" - AuthorizationMiddleware.redirectToRestricted req, res, next - - _getUserAndProjectId: (req, callback = (error, user_id, project_id) ->) -> - project_id = req.params?.project_id or req.params?.Project_id - if !project_id? - return callback(new Error("Expected project_id in request parameters")) - if !ObjectId.isValid(project_id) - return callback(new Errors.NotFoundError("invalid project_id: #{project_id}")) - AuthorizationMiddleware._getUserId req, (error, user_id) -> - return callback(error) if error? - callback(null, user_id, project_id) - - _getUserId: (req, callback = (error, user_id) ->) -> - user_id = AuthenticationController.getLoggedInUserId(req) || req?.oauth_user?._id || null - return callback(null, user_id) - - redirectToRestricted: (req, res, next) -> - # TODO: move this to throwing ForbiddenError - res.redirect "/restricted?from=#{encodeURIComponent(req.url)}" - - restricted : (req, res, next)-> - if AuthenticationController.isUserLoggedIn(req) - res.render 'user/restricted', - title:'restricted' - else - from = req.query.from - logger.log {from: from}, "redirecting to login" - redirect_to = "/login" - if from? - AuthenticationController.setRedirectInSession(req, from) - res.redirect redirect_to diff --git a/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee b/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee deleted file mode 100644 index 682ae08a02..0000000000 --- a/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = - NONE: false - READ_ONLY: "readOnly" - READ_AND_WRITE: "readAndWrite" - OWNER: "owner" \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee b/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee deleted file mode 100644 index e31426221e..0000000000 --- a/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = - READ_ONLY: "readOnly" # LEGACY - READ_AND_WRITE: "readAndWrite" # LEGACY - PRIVATE: "private" - TOKEN_BASED: "tokenBased" diff --git a/services/web/app/coffee/Features/Authorization/Sources.coffee b/services/web/app/coffee/Features/Authorization/Sources.coffee deleted file mode 100644 index 149d296211..0000000000 --- a/services/web/app/coffee/Features/Authorization/Sources.coffee +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = - INVITE: 'invite' - TOKEN: 'token' - OWNER: 'owner' diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee deleted file mode 100644 index 1e0577cfc1..0000000000 --- a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee +++ /dev/null @@ -1,40 +0,0 @@ -BetaProgramHandler = require './BetaProgramHandler' -UserGetter = require "../User/UserGetter" -Settings = require "settings-sharelatex" -logger = require 'logger-sharelatex' -AuthenticationController = require '../Authentication/AuthenticationController' - - -module.exports = BetaProgramController = - - optIn: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - 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 = AuthenticationController.getLoggedInUserId(req) - 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 = AuthenticationController.getLoggedInUserId(req) - logger.log {user_id}, "showing beta participation page for user" - UserGetter.getUser 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, diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee deleted file mode 100644 index 0c902fcfe1..0000000000 --- a/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee +++ /dev/null @@ -1,31 +0,0 @@ -User = require("../../models/User").User -logger = require 'logger-sharelatex' -metrics = require("metrics-sharelatex") - -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) diff --git a/services/web/app/coffee/Features/Blog/BlogController.coffee b/services/web/app/coffee/Features/Blog/BlogController.coffee deleted file mode 100644 index eb2b3fad94..0000000000 --- a/services/web/app/coffee/Features/Blog/BlogController.coffee +++ /dev/null @@ -1,45 +0,0 @@ -request = require("request") -settings = require("settings-sharelatex") -logger = require("logger-sharelatex") -_ = require("underscore") -ErrorController = require "../Errors/ErrorController" - -module.exports = BlogController = - - getPage: (req, res, next)-> - url = req.url?.toLowerCase() - blogUrl = "#{settings.apis.blog.url}#{url}" - - extensionsToProxy = [".png", ".xml", ".jpeg", ".jpg", ".json", ".zip", ".eps", ".gif"] - - shouldProxy = _.find extensionsToProxy, (extension)-> - url.indexOf(extension) != -1 - - if shouldProxy - return BlogController._directProxy blogUrl, res - - logger.log url:url, "proxying request to blog api" - request.get blogUrl, (err, r, data)-> - 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 - - - getIndexPage: (req, res)-> - req.url = "/blog/index.html" - BlogController.getPage req, res - - _directProxy: (originUrl, res)-> - upstream = request.get(originUrl) - upstream.on "error", (error) -> - logger.error err: error, "blog proxy error" - upstream.pipe res diff --git a/services/web/app/coffee/Features/Blog/BlogHandler.coffee b/services/web/app/coffee/Features/Blog/BlogHandler.coffee deleted file mode 100644 index c3fd33b8eb..0000000000 --- a/services/web/app/coffee/Features/Blog/BlogHandler.coffee +++ /dev/null @@ -1,23 +0,0 @@ -request = require "request" -settings = require "settings-sharelatex" -_ = require("underscore") -logger = require "logger-sharelatex" - -module.exports = BlogHandler = - - getLatestAnnouncements: (callback)-> - blogUrl = "#{settings.apis.blog.url}/blog/latestannouncements.json" - opts = - url:blogUrl - json:true - timeout:1000 - request.get opts, (err, res, announcements)-> - if err? - return callback err - if res.statusCode != 200 - return callback("blog announcement returned non 200") - logger.log announcementsLength: announcements?.length, "announcements returned" - announcements = _.map announcements, (announcement)-> - announcement.date = new Date(announcement.date) - return announcement - callback(err, announcements) diff --git a/services/web/app/coffee/Features/BrandVariations/BrandVariationsHandler.coffee b/services/web/app/coffee/Features/BrandVariations/BrandVariationsHandler.coffee deleted file mode 100644 index 2836ee320b..0000000000 --- a/services/web/app/coffee/Features/BrandVariations/BrandVariationsHandler.coffee +++ /dev/null @@ -1,41 +0,0 @@ -url = require "url" -settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -V1Api = require "../V1/V1Api" - -module.exports = BrandVariationsHandler = - getBrandVariationById: (brandVariationId, callback = (error, brandVariationDetails) ->)-> - if !brandVariationId? or brandVariationId == "" - return callback(new Error("Branding variation id not provided")) - logger.log brandVariationId: brandVariationId, "fetching brand variation details from v1" - V1Api.request { - uri: "/api/v2/brand_variations/#{brandVariationId}" - }, (error, response, brandVariationDetails) -> - if error? - logger.err { brandVariationId, error}, "error getting brand variation details" - return callback(error) - _formatBrandVariationDetails brandVariationDetails - callback(null, brandVariationDetails) - -_formatBrandVariationDetails = (details) -> - if details.export_url? - details.export_url = _setV1AsHostIfRelativeURL details.export_url - if details.home_url? - details.home_url = _setV1AsHostIfRelativeURL details.home_url - if details.logo_url? - details.logo_url = _setV1AsHostIfRelativeURL details.logo_url - if details.journal_guidelines_url? - details.journal_guidelines_url = _setV1AsHostIfRelativeURL details.journal_guidelines_url - if details.journal_cover_url? - details.journal_cover_url = _setV1AsHostIfRelativeURL details.journal_cover_url - if details.submission_confirmation_page_logo_url? - details.submission_confirmation_page_logo_url = _setV1AsHostIfRelativeURL details.submission_confirmation_page_logo_url - if details.publish_menu_icon? - details.publish_menu_icon = _setV1AsHostIfRelativeURL details.publish_menu_icon - -_setV1AsHostIfRelativeURL = (urlString) -> - # The first argument is the base URL to resolve against if the second argument is not absolute. - # As it only applies if the second argument is not absolute, we can use it to transform relative URLs into - # absolute ones using v1 as the host. If the URL is absolute (e.g. a filepicker one), then the base - # argument is just ignored - url.resolve settings?.apis?.v1?.url, urlString \ No newline at end of file diff --git a/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee b/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee deleted file mode 100644 index abe389daa7..0000000000 --- a/services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee +++ /dev/null @@ -1,28 +0,0 @@ -request = require 'request' -logger = require 'logger-sharelatex' -Settings = require 'settings-sharelatex' - -module.exports = CaptchaMiddleware = - validateCaptcha: (action) -> - return (req, res, next) -> - if !Settings.recaptcha?.siteKey? - return next() - inviteAndCaptchaDisabled = action == 'invite' and Settings.recaptcha.disabled.invite - registerAndCaptchaDisabled = action == 'register' and Settings.recaptcha.disabled.register - if inviteAndCaptchaDisabled or registerAndCaptchaDisabled - return next() - response = req.body['g-recaptcha-response'] - options = - form: - secret: Settings.recaptcha.secretKey - response: response - json: true - request.post "https://www.google.com/recaptcha/api/siteverify", options, (error, response, body) -> - return next(error) if error? - if !body?.success - logger.warn {statusCode: response.statusCode, body: body}, 'failed recaptcha siteverify request' - return res.status(400).send({errorReason:"cannot_verify_user_not_robot", message: - {text:"Sorry, we could not verify that you are not a robot. Please check that Google reCAPTCHA is not being blocked by an ad blocker or firewall."} - }) - else - return next() diff --git a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee deleted file mode 100644 index 3cae19b7f3..0000000000 --- a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee +++ /dev/null @@ -1,82 +0,0 @@ -request = require("request") -settings = require("settings-sharelatex") -logger = require("logger-sharelatex") - -module.exports = ChatApiHandler = - _apiRequest: (opts, callback = (error, data) ->) -> - request opts, (error, response, data) -> - return callback(error) if error? - if 200 <= response.statusCode < 300 - return callback null, data - else - error = new Error("chat api returned non-success code: #{response.statusCode}") - error.statusCode = response.statusCode - logger.error {err: error, opts}, "error sending request to chat api" - return callback error - - sendGlobalMessage: (project_id, user_id, content, callback)-> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages" - method: "POST" - json: {user_id, content} - }, callback - - getGlobalMessages: (project_id, limit, before, callback)-> - qs = {} - qs.limit = limit if limit? - qs.before = before if before? - - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages" - method: "GET" - qs: qs - json: true - }, callback - - sendComment: (project_id, thread_id, user_id, content, callback = (error) ->) -> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages" - method: "POST" - json: {user_id, content} - }, callback - - getThreads: (project_id, callback = (error) ->) -> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads" - method: "GET" - json: true - }, callback - - resolveThread: (project_id, thread_id, user_id, callback = (error) ->) -> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/resolve" - method: "POST" - json: {user_id} - }, callback - - reopenThread: (project_id, thread_id, callback = (error) ->) -> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen" - method: "POST" - }, callback - - deleteThread: (project_id, thread_id, callback = (error) ->) -> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}" - method: "DELETE" - }, callback - - editMessage: (project_id, thread_id, message_id, content, callback = (error) ->) -> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}/edit" - method: "POST" - json: - content: content - }, callback - - deleteMessage: (project_id, thread_id, message_id, callback = (error) ->) -> - ChatApiHandler._apiRequest { - url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}" - method: "DELETE" - }, callback - \ No newline at end of file diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee deleted file mode 100644 index ca5b84b024..0000000000 --- a/services/web/app/coffee/Features/Chat/ChatController.coffee +++ /dev/null @@ -1,65 +0,0 @@ -ChatApiHandler = require("./ChatApiHandler") -EditorRealTimeController = require("../Editor/EditorRealTimeController") -logger = require("logger-sharelatex") -AuthenticationController = require('../Authentication/AuthenticationController') -UserInfoManager = require('../User/UserInfoManager') -UserInfoController = require('../User/UserInfoController') -async = require "async" - -module.exports = ChatController = - sendMessage: (req, res, next)-> - project_id = req.params.project_id - content = req.body.content - user_id = AuthenticationController.getLoggedInUserId(req) - if !user_id? - err = new Error('no logged-in user') - return next(err) - ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) -> - return next(err) if err? - UserInfoManager.getPersonalInfo message.user_id, (err, user) -> - return next(err) if err? - message.user = UserInfoController.formatPersonalInfo(user) - EditorRealTimeController.emitToRoom project_id, "new-chat-message", message - res.send(204) - - getMessages: (req, res, next)-> - project_id = req.params.project_id - query = req.query - logger.log project_id:project_id, query:query, "getting messages" - ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) -> - return next(err) if err? - ChatController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) -> - return next(err) if err? - logger.log length: messages?.length, "sending messages to client" - res.json messages - - _injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) -> - # There will be a lot of repitition of user_ids, so first build a list - # of unique ones to perform db look ups on, then use these to populate the - # user fields - user_ids = {} - for thread_id, thread of threads - if thread.resolved - user_ids[thread.resolved_by_user_id] = true - for message in thread.messages - user_ids[message.user_id] = true - - jobs = [] - users = {} - for user_id, _ of user_ids - do (user_id) -> - jobs.push (cb) -> - UserInfoManager.getPersonalInfo user_id, (err, user) -> - return cb(error) if error? - user = UserInfoController.formatPersonalInfo user - users[user_id] = user - cb() - - async.series jobs, (error) -> - return callback(error) if error? - for thread_id, thread of threads - if thread.resolved - thread.resolved_by_user = users[thread.resolved_by_user_id] - for message in thread.messages - message.user = users[message.user_id] - return callback null, threads \ No newline at end of file diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee deleted file mode 100644 index 1d5a147c95..0000000000 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ /dev/null @@ -1,40 +0,0 @@ -ProjectGetter = require "../Project/ProjectGetter" -CollaboratorsHandler = require "./CollaboratorsHandler" -ProjectEditorHandler = require "../Project/ProjectEditorHandler" -EditorRealTimeController = require "../Editor/EditorRealTimeController" -LimitationsManager = require "../Subscription/LimitationsManager" -UserGetter = require "../User/UserGetter" -EmailHelper = require "../Helpers/EmailHelper" -logger = require 'logger-sharelatex' - - -module.exports = CollaboratorsController = - removeUserFromProject: (req, res, next) -> - project_id = req.params.Project_id - user_id = req.params.user_id - CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) -> - return next(error) if error? - EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true} - res.sendStatus 204 - - removeSelfFromProject: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - user_id = req.session?.user?._id - CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) -> - return next(error) if error? - res.sendStatus 204 - - _removeUserIdFromProject: (project_id, user_id, callback = (error) ->) -> - CollaboratorsHandler.removeUserFromProject project_id, user_id, (error)-> - return callback(error) if error? - EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) - callback() - - getAllMembers: (req, res, next) -> - projectId = req.params.Project_id - logger.log {projectId}, "getting all active members for project" - CollaboratorsHandler.getAllInvitedMembers projectId, (err, members) -> - if err? - logger.err {projectId}, "error getting members for project" - return next(err) - res.json({members: members}) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee deleted file mode 100644 index 913562f417..0000000000 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee +++ /dev/null @@ -1,28 +0,0 @@ -Project = require("../../models/Project").Project -EmailHandler = require("../Email/EmailHandler") -Settings = require "settings-sharelatex" - - -module.exports = CollaboratorsEmailHandler = - - _buildInviteUrl: (project, invite) -> - "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [ - "project_name=#{encodeURIComponent(project.name)}" - "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}" - ].join("&") - - notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)-> - Project - .findOne(_id: project_id ) - .select("name owner_ref") - .populate('owner_ref') - .exec (err, project)-> - emailOptions = - to: email - replyTo: project.owner_ref.email - project: - name: project.name - inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite) - owner: project.owner_ref - sendingUser_id: sendingUser._id - EmailHandler.sendEmail "projectInvite", emailOptions, callback diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee deleted file mode 100644 index 18f68a260c..0000000000 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ /dev/null @@ -1,307 +0,0 @@ -UserCreator = require('../User/UserCreator') -Project = require("../../models/Project").Project -ProjectGetter = require('../Project/ProjectGetter') -logger = require('logger-sharelatex') -UserGetter = require "../User/UserGetter" -ContactManager = require "../Contacts/ContactManager" -CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" -async = require "async" -PrivilegeLevels = require "../Authorization/PrivilegeLevels" -PublicAccessLevels = require "../Authorization/PublicAccessLevels" -Errors = require "../Errors/Errors" -EmailHelper = require "../Helpers/EmailHelper" -ProjectEditorHandler = require "../Project/ProjectEditorHandler" -Sources = require "../Authorization/Sources" -ObjectId = require('mongojs').ObjectId - -module.exports = CollaboratorsHandler = - - getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - projection = - owner_ref: 1, - collaberator_refs: 1, - readOnly_refs: 1, - tokenAccessReadOnly_refs: 1, - tokenAccessReadAndWrite_refs: 1 - publicAccesLevel: 1 - ProjectGetter.getProject project_id, projection, (error, project) -> - return callback(error) if error? - return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project? - members = [] - members.push { - id: project.owner_ref.toString(), - privilegeLevel: PrivilegeLevels.OWNER, - source: Sources.OWNER - } - for member_id in project.collaberator_refs or [] - members.push { - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_AND_WRITE, - source: Sources.INVITE - } - for member_id in project.readOnly_refs or [] - members.push { - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_ONLY, - source: Sources.INVITE - } - if project.publicAccesLevel == PublicAccessLevels.TOKEN_BASED - for member_id in project.tokenAccessReadAndWrite_refs or [] - members.push { - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_AND_WRITE, - source: Sources.TOKEN - } - for member_id in project.tokenAccessReadOnly_refs or [] - members.push { - id: member_id.toString(), - privilegeLevel: PrivilegeLevels.READ_ONLY, - source: Sources.TOKEN - } - return callback null, members - - getMemberIds: (project_id, callback = (error, member_ids) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> - return callback(error) if error? - return callback null, members.map (m) -> m.id - - getInvitedMemberIds: (project_id, callback = (error, member_ids) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> - return callback(error) if error? - return callback null, members.filter((m) -> m.source != Sources.TOKEN).map((m) -> m.id) - - USER_PROJECTION: { - _id: 1, - email: 1, - features: 1, - first_name: 1, - last_name: 1, - signUpDate: 1 - } - _loadMembers: (members, callback=(error, users) ->) -> - result = [] - async.mapLimit members, 3, - (member, cb) -> - UserGetter.getUserOrUserStubById member.id, CollaboratorsHandler.USER_PROJECTION, (error, user) -> - return cb(error) if error? - if user? - result.push { user: user, privilegeLevel: member.privilegeLevel } - cb() - (error) -> - return callback(error) if error? - callback null, result - - getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - CollaboratorsHandler._loadMembers members, (error, users) -> - callback error, users - - getInvitedMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - members = members.filter((m) -> m.source != Sources.TOKEN) - CollaboratorsHandler._loadMembers members, (error, users) -> - callback error, users - - getTokenMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - members = members.filter((m) -> m.source == Sources.TOKEN) - CollaboratorsHandler._loadMembers members, (error, users) -> - callback error, users - - getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> - # In future if the schema changes and getting all member ids is more expensive (multiple documents) - # then optimise this. - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - for member in members - if member.id == user_id?.toString() - return callback null, member.privilegeLevel - return callback null, PrivilegeLevels.NONE - - getInvitedMemberCount: (project_id, callback = (error, count) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> - return callback(error) if error? - return callback null, (members or []).filter((m) -> m.source != Sources.TOKEN).length - - getInvitedCollaboratorCount: (project_id, callback = (error, count) ->) -> - CollaboratorsHandler.getInvitedMemberCount project_id, (error, count) -> - return callback(error) if error? - return callback null, count - 1 # Don't count project owner - - isUserInvitedMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> - return callback(error) if error? - for member in members - if member.id.toString() == user_id.toString() and member.source != Sources.TOKEN - return callback null, true, member.privilegeLevel - return callback null, false, null - - getProjectsUserIsMemberOf: (user_id, fields, callback = (error, {readAndWrite:[],readOnly:[],tokenReadAndWrite:[],tokenReadOnly:[]}) ->) -> - async.mapLimit( - [ - {collaberator_refs: user_id}, - {readOnly_refs: user_id}, - { - tokenAccessReadAndWrite_refs: user_id, - publicAccesLevel: PublicAccessLevels.TOKEN_BASED - }, - { - tokenAccessReadOnly_refs: user_id, - publicAccesLevel: PublicAccessLevels.TOKEN_BASED - } - ] - , 2 - , (query, cb) -> - Project.find query, fields, cb - , (error, results) -> - return callback(error) if error? - projects = { - readAndWrite: results[0] - readOnly: results[1] - tokenReadAndWrite: results[2] - tokenReadOnly: results[3] - } - callback(null, projects) - ) - - removeUserFromProject: (project_id, user_id, callback = (error) ->)-> - logger.log user_id: user_id, project_id: project_id, "removing user" - conditions = _id:project_id - update = $pull:{} - update["$pull"] = - collaberator_refs:user_id, - readOnly_refs:user_id - tokenAccessReadOnly_refs:user_id - tokenAccessReadAndWrite_refs:user_id - Project.update conditions, update, (err)-> - if err? - logger.error err: err, "problem removing user from project collaberators" - callback(err) - - removeUserFromAllProjets: (user_id, callback = (error) ->) -> - CollaboratorsHandler.getProjectsUserIsMemberOf user_id, { _id: 1 }, (error, {readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly}) -> - return callback(error) if error? - allProjects = - readAndWrite - .concat(readOnly) - .concat(tokenReadAndWrite) - .concat(tokenReadOnly) - 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 - - addUserIdToProject: (project_id, adding_user_id, user_id, privilegeLevel, callback = (error) ->)-> - ProjectGetter.getProject project_id, { collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> - return callback(error) if error? - existing_users = (project.collaberator_refs or []) - existing_users = existing_users.concat(project.readOnly_refs or []) - existing_users = existing_users.map (u) -> u.toString() - if existing_users.indexOf(user_id.toString()) > -1 - return callback null # User already in Project - - if privilegeLevel == PrivilegeLevels.READ_AND_WRITE - level = {"collaberator_refs":user_id} - logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user" - else if privilegeLevel == PrivilegeLevels.READ_ONLY - level = {"readOnly_refs":user_id} - logger.log {privileges: "readOnly", user_id, project_id}, "adding user" - else - return callback(new Error("unknown privilegeLevel: #{privilegeLevel}")) - - if adding_user_id - ContactManager.addContact adding_user_id, user_id - - Project.update { _id: project_id }, { $addToSet: level }, (error) -> - return callback(error) if error? - # Flush to TPDS in background to add files to collaborator's Dropbox - ProjectEntityHandler = require("../Project/ProjectEntityHandler") - ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) -> - if error? - logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" - callback() - - getAllInvitedMembers: (projectId, callback=(err, members)->) -> - logger.log {projectId}, "fetching all members" - CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels projectId, (error, rawMembers) -> - if error? - logger.err {projectId, error}, "error getting members for project" - return callback(error) - {owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) - callback(null, members) - - userIsTokenMember: (userId, projectId, callback=(err, isTokenMember)->) -> - userId = ObjectId(userId.toString()) - projectId = ObjectId(projectId.toString()) - Project.findOne { - _id: projectId, - $or: [ - {tokenAccessReadOnly_refs: userId}, - {tokenAccessReadAndWrite_refs: userId} - ] - }, { - _id: 1 - }, (err, project) -> - if err? - return callback(err) - callback(null, project?) - - transferProjects: (from_user_id, to_user_id, callback=(err, projects) ->) -> - MEMBER_KEYS = ['collaberator_refs', 'readOnly_refs'] - - # Find all the projects this user is part of so we can flush them to TPDS - query = - $or: - [{ owner_ref: from_user_id }] - .concat( - MEMBER_KEYS.map (key) -> - q = {} - q[key] = from_user_id - return q - ) # [{ collaberator_refs: from_user_id }, ...] - Project.find query, { _id: 1 }, (error, projects = []) -> - return callback(error) if error? - - project_ids = projects.map (p) -> p._id - logger.log {project_ids, from_user_id, to_user_id}, "transferring projects" - - update_jobs = [] - update_jobs.push (cb) -> - Project.update { owner_ref: from_user_id }, { $set: { owner_ref: to_user_id }}, { multi: true }, cb - for key in MEMBER_KEYS - do (key) -> - update_jobs.push (cb) -> - query = {} - addNewUserUpdate = $addToSet: {} - removeOldUserUpdate = $pull: {} - query[key] = from_user_id - removeOldUserUpdate.$pull[key] = from_user_id - addNewUserUpdate.$addToSet[key] = to_user_id - # Mongo won't let us pull and addToSet in the same query, so do it in - # two. Note we need to add first, since the query is based on the old user. - Project.update query, addNewUserUpdate, { multi: true }, (error) -> - return cb(error) if error? - Project.update query, removeOldUserUpdate, { multi: true }, cb - - # Flush each project to TPDS to add files to new user's Dropbox - ProjectEntityHandler = require("../Project/ProjectEntityHandler") - flush_jobs = [] - for project_id in project_ids - do (project_id) -> - flush_jobs.push (cb) -> - ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, cb - - # Flush in background, no need to block on this - async.series flush_jobs, (error) -> - if error? - logger.err {err: error, project_ids, from_user_id, to_user_id}, "error flushing tranferred projects to TPDS" - - async.series update_jobs, (err) -> - logger.log("flushed transferred projects to TPDS") - callback(err) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee deleted file mode 100644 index de4710e19a..0000000000 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ /dev/null @@ -1,171 +0,0 @@ -ProjectGetter = require "../Project/ProjectGetter" -LimitationsManager = require "../Subscription/LimitationsManager" -UserGetter = require "../User/UserGetter" -CollaboratorsHandler = require('./CollaboratorsHandler') -CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') -logger = require('logger-sharelatex') -Settings = require('settings-sharelatex') -EmailHelper = require "../Helpers/EmailHelper" -EditorRealTimeController = require("../Editor/EditorRealTimeController") -NotificationsBuilder = require("../Notifications/NotificationsBuilder") -AnalyticsManger = require("../Analytics/AnalyticsManager") -AuthenticationController = require("../Authentication/AuthenticationController") -rateLimiter = require("../../infrastructure/RateLimiter") -request = require 'request' - -module.exports = CollaboratorsInviteController = - - getAllInvites: (req, res, next) -> - projectId = req.params.Project_id - logger.log {projectId}, "getting all active invites for project" - CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) -> - if err? - logger.err {projectId}, "error getting invites for project" - return next(err) - res.json({invites: invites}) - - _checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) -> - if Settings.restrictInvitesToExistingAccounts == true - logger.log {email}, "checking if user exists with this email" - UserGetter.getUserByAnyEmail email, {_id: 1}, (err, user) -> - return callback(err) if err? - userExists = user? and user?._id? - callback(null, userExists) - else - callback(null, true) - - _checkRateLimit: (user_id, callback = (error) ->) -> - LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)-> - return callback(err) if err? - if collabLimit == -1 - collabLimit = 20 - collabLimit = collabLimit * 10 - opts = - endpointName: "invite-to-project-by-user-id" - timeInterval: 60 * 30 - subjectName: user_id - throttle: collabLimit - rateLimiter.addCount opts, callback - - inviteToProject: (req, res, next) -> - projectId = req.params.Project_id - email = req.body.email - sendingUser = AuthenticationController.getSessionUser(req) - sendingUserId = sendingUser._id - if email == sendingUser.email - logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project" - return res.json {invite: null, error: 'cannot_invite_self'} - logger.log {projectId, email, sendingUserId}, "inviting to project" - LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => - return next(error) if error? - if !allowed - logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" - return res.json {invite: null} - {email, privileges} = req.body - email = EmailHelper.parseEmail(email) - if !email? or email == "" - logger.log {projectId, email, sendingUserId}, "invalid email address" - return res.status(400).send({errorReason:"invalid_email"}) - CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) -> - return next(error) if error? - if !underRateLimit - return res.sendStatus(429) - CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)-> - if err? - logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" - return next(err) - if !shouldAllowInvite - logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address" - return res.json {invite: null, error: 'cannot_invite_non_user'} - CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> - if err? - logger.err {projectId, email, sendingUserId}, "error creating project invite" - return next(err) - logger.log {projectId, email, sendingUserId}, "invite created" - EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) - return res.json {invite: invite} - - revokeInvite: (req, res, next) -> - projectId = req.params.Project_id - inviteId = req.params.invite_id - logger.log {projectId, inviteId}, "revoking invite" - CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) -> - if err? - logger.err {projectId, inviteId}, "error revoking invite" - return next(err) - EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true} - res.sendStatus(201) - - resendInvite: (req, res, next) -> - projectId = req.params.Project_id - inviteId = req.params.invite_id - logger.log {projectId, inviteId}, "resending invite" - sendingUser = AuthenticationController.getSessionUser(req) - CollaboratorsInviteController._checkRateLimit sendingUser._id, (error, underRateLimit) -> - return next(error) if error? - if !underRateLimit - return res.sendStatus(429) - CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) -> - if err? - logger.err {projectId, inviteId}, "error resending invite" - return next(err) - res.sendStatus(201) - - viewInvite: (req, res, next) -> - projectId = req.params.Project_id - token = req.params.token - _renderInvalidPage = () -> - logger.log {projectId, token}, "invite not valid, rendering not-valid page" - res.render "project/invite/not-valid", {title: "Invalid Invite"} - # check if the user is already a member of the project - currentUser = AuthenticationController.getSessionUser(req) - CollaboratorsHandler.isUserInvitedMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) -> - if err? - logger.err {err, projectId}, "error checking if user is member of project" - return next(err) - if isMember - logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting" - return res.redirect "/project/#{projectId}" - # get the invite - CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) -> - if err? - logger.err {projectId, token}, "error getting invite by token" - return next(err) - # check if invite is gone, or otherwise non-existent - if !invite? - logger.log {projectId, token}, "no invite found for this token" - return _renderInvalidPage() - # check the user who sent the invite exists - UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> - if err? - logger.err {err, projectId}, "error getting project owner" - return next(err) - if !owner? - logger.log {projectId}, "no project owner found" - return _renderInvalidPage() - # fetch the project name - ProjectGetter.getProject projectId, {}, (err, project) -> - if err? - logger.err {err, projectId}, "error getting project" - return next(err) - if !project? - logger.log {projectId}, "no project found" - return _renderInvalidPage() - # finally render the invite - res.render "project/invite/show", {invite, project, owner, title: "Project Invite"} - - acceptInvite: (req, res, next) -> - projectId = req.params.Project_id - token = req.params.token - currentUser = AuthenticationController.getSessionUser(req) - logger.log {projectId, userId: currentUser._id, token}, "got request to accept invite" - CollaboratorsInviteHandler.acceptInvite projectId, token, currentUser, (err) -> - if err? - logger.err {projectId, token}, "error accepting invite by token" - return next(err) - EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true} - AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {projectId:projectId, userId:currentUser._id}) - if req.xhr - res.sendStatus 204 # Done async via project page notification - else - res.redirect "/project/#{projectId}" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee deleted file mode 100644 index 58121dae2c..0000000000 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ /dev/null @@ -1,144 +0,0 @@ -ProjectInvite = require("../../models/ProjectInvite").ProjectInvite -logger = require('logger-sharelatex') -CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" -CollaboratorsHandler = require "./CollaboratorsHandler" -UserGetter = require "../User/UserGetter" -ProjectGetter = require "../Project/ProjectGetter" -Async = require "async" -PrivilegeLevels = require "../Authorization/PrivilegeLevels" -Errors = require "../Errors/Errors" -Crypto = require 'crypto' -NotificationsBuilder = require("../Notifications/NotificationsBuilder") - - -module.exports = CollaboratorsInviteHandler = - - getAllInvites: (projectId, callback=(err, invites)->) -> - logger.log {projectId}, "fetching invites for project" - ProjectInvite.find {projectId: projectId}, (err, invites) -> - if err? - logger.err {err, projectId}, "error getting invites from mongo" - return callback(err) - logger.log {projectId, count: invites.length}, "found invites for project" - callback(null, invites) - - getInviteCount: (projectId, callback=(err, count)->) -> - logger.log {projectId}, "counting invites for project" - ProjectInvite.count {projectId: projectId}, (err, count) -> - if err? - logger.err {err, projectId}, "error getting invites from mongo" - return callback(err) - callback(null, count) - - _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) -> - email = invite.email - UserGetter.getUserByAnyEmail email, {_id: 1}, (err, existingUser) -> - if err? - logger.err {projectId, email}, "error checking if user exists" - return callback(err) - if !existingUser? - logger.log {projectId, email}, "no existing user found, returning" - return callback(null) - ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) -> - if err? - logger.err {projectId, email}, "error getting project" - return callback(err) - if !project? - logger.log {projectId}, "no project found while sending notification, returning" - return callback(null) - NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) - - _tryCancelInviteNotification: (inviteId, callback=()->) -> - NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback) - - _sendMessages: (projectId, sendingUser, invite, callback=(err)->) -> - logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite" - CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, sendingUser, (err)-> - return callback(err) if err? - CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)-> - return callback(err) if err? - callback() - - inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) -> - logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite" - Crypto.randomBytes 24, (err, buffer) -> - if err? - logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token" - return callback(err) - token = buffer.toString('hex') - invite = new ProjectInvite { - email: email - token: token - sendingUserId: sendingUser._id - projectId: projectId - privileges: privileges - } - invite.save (err, invite) -> - if err? - logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token" - return callback(err) - # Send email and notification in background - CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> - if err? - logger.err {err, projectId, email}, "error sending messages for invite" - callback(null, invite) - - - revokeInvite: (projectId, inviteId, callback=(err)->) -> - logger.log {projectId, inviteId}, "removing invite" - ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) -> - if err? - logger.err {err, projectId, inviteId}, "error removing invite" - return callback(err) - CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->) - callback(null) - - resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) -> - logger.log {projectId, inviteId}, "resending invite email" - ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) -> - if err? - logger.err {err, projectId, inviteId}, "error finding invite" - return callback(err) - if !invite? - logger.err {err, projectId, inviteId}, "no invite found, nothing to resend" - return callback(null) - CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> - if err? - logger.err {projectId, inviteId}, "error resending invite messages" - return callback(err) - callback(null) - - getInviteByToken: (projectId, tokenString, callback=(err,invite)->) -> - logger.log {projectId, tokenString}, "fetching invite by token" - ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) -> - if err? - logger.err {err, projectId}, "error fetching invite" - return callback(err) - if !invite? - logger.err {err, projectId, token: tokenString}, "no invite found" - return callback(null, null) - callback(null, invite) - - acceptInvite: (projectId, tokenString, user, callback=(err)->) -> - logger.log {projectId, userId: user._id, tokenString}, "accepting invite" - CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) -> - if err? - logger.err {err, projectId, tokenString}, "error finding invite" - return callback(err) - if !invite - err = new Errors.NotFoundError("no matching invite found") - logger.log {err, projectId, tokenString}, "no matching invite found" - return callback(err) - inviteId = invite._id - CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) -> - if err? - logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project" - return callback(err) - # Remove invite - logger.log {projectId, inviteId}, "removing invite" - ProjectInvite.remove {_id: inviteId}, (err) -> - if err? - logger.err {err, projectId, inviteId}, "error removing invite" - return callback(err) - CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()-> - callback() diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee deleted file mode 100644 index 82d1b0fc9b..0000000000 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ /dev/null @@ -1,79 +0,0 @@ -CollaboratorsController = require('./CollaboratorsController') -AuthenticationController = require('../Authentication/AuthenticationController') -AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') -CollaboratorsInviteController = require('./CollaboratorsInviteController') -RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') -CaptchaMiddleware = require '../Captcha/CaptchaMiddleware' - -module.exports = - apply: (webRouter, apiRouter) -> - webRouter.post '/project/:Project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject - - webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddleware.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject - - webRouter.get( - '/project/:Project_id/members', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - CollaboratorsController.getAllMembers - ) - - # invites - webRouter.post( - '/project/:Project_id/invite', - RateLimiterMiddleware.rateLimit({ - endpointName: "invite-to-project-by-project-id" - params: ["Project_id"] - maxRequests: 100 - timeInterval: 60 * 10 - }), - RateLimiterMiddleware.rateLimit({ - endpointName: "invite-to-project-by-ip" - ipOnly:true - maxRequests: 100 - timeInterval: 60 * 10 - }), - CaptchaMiddleware.validateCaptcha('invite'), - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - CollaboratorsInviteController.inviteToProject - ) - - webRouter.get( - '/project/:Project_id/invites', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - CollaboratorsInviteController.getAllInvites - ) - - webRouter.delete( - '/project/:Project_id/invite/:invite_id', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - CollaboratorsInviteController.revokeInvite - ) - - webRouter.post( - '/project/:Project_id/invite/:invite_id/resend', - RateLimiterMiddleware.rateLimit({ - endpointName: "resend-invite" - params: ["Project_id"] - maxRequests: 200 - timeInterval: 60 * 10 - }), - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - CollaboratorsInviteController.resendInvite - ) - - webRouter.get( - '/project/:Project_id/invite/token/:token', - AuthenticationController.requireLogin(), - CollaboratorsInviteController.viewInvite - ) - - webRouter.post( - '/project/:Project_id/invite/token/:token/accept', - AuthenticationController.requireLogin(), - CollaboratorsInviteController.acceptInvite - ) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee deleted file mode 100644 index f2c982a077..0000000000 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ /dev/null @@ -1,81 +0,0 @@ -Settings = require "settings-sharelatex" -request = require('request') -RedisWrapper = require("../../infrastructure/RedisWrapper") -rclient = RedisWrapper.client("clsi_cookie") -if Settings.redis.clsi_cookie_secondary? - rclient_secondary = RedisWrapper.client("clsi_cookie_secondary") -Cookie = require('cookie') -logger = require "logger-sharelatex" - - -clsiCookiesEnabled = Settings.clsiCookie?.key? and Settings.clsiCookie.key.length != 0 - - -module.exports = (backendGroup)-> - - buildKey : (project_id)-> - if backendGroup? - return "clsiserver:#{backendGroup}:#{project_id}" - else - return "clsiserver:#{project_id}" - - _getServerId : (project_id, callback = (err, serverId)->)-> - rclient.get @buildKey(project_id), (err, serverId)=> - if err? - return callback(err) - if !serverId? or serverId == "" - return @_populateServerIdViaRequest project_id, callback - else - return callback(null, serverId) - - - _populateServerIdViaRequest :(project_id, callback = (err, serverId)->)-> - url = "#{Settings.apis.clsi.url}/project/#{project_id}/status" - request.get url, (err, res, body)=> - if err? - logger.err err:err, project_id:project_id, "error getting initial server id for project" - return callback(err) - @setServerId project_id, res, (err, serverId)-> - if err? - logger.err err:err, project_id:project_id, "error setting server id via populate request" - callback(err, serverId) - - _parseServerIdFromResponse : (response)-> - cookies = Cookie.parse(response.headers["set-cookie"]?[0] or "") - return cookies?[Settings.clsiCookie.key] - - setServerId: (project_id, response, callback = (err, serverId)->)-> - if !clsiCookiesEnabled - return callback() - serverId = @_parseServerIdFromResponse(response) - if !serverId? # We don't get a cookie back if it hasn't changed - return rclient.expire(@buildKey(project_id), Settings.clsiCookie.ttl, callback) - if rclient_secondary? - @_setServerIdInRedis rclient_secondary, project_id, serverId - @_setServerIdInRedis rclient, project_id, serverId, (err) -> - callback(err, serverId) - - _setServerIdInRedis: (rclient, project_id, serverId, callback = (err) ->) -> - multi = rclient.multi() - multi.set @buildKey(project_id), serverId - multi.expire @buildKey(project_id), Settings.clsiCookie.ttl - multi.exec callback - - clearServerId: (project_id, callback = (err)->)-> - if !clsiCookiesEnabled - return callback() - rclient.del @buildKey(project_id), callback - - getCookieJar: (project_id, callback = (err, jar)->)-> - if !clsiCookiesEnabled - return callback(null, request.jar()) - @_getServerId project_id, (err, serverId)=> - if err? - logger.err err:err, project_id:project_id, "error getting server id" - return callback(err) - serverCookie = request.cookie("#{Settings.clsiCookie.key}=#{serverId}") - jar = request.jar() - jar.setCookie serverCookie, Settings.apis.clsi.url - callback(null, jar) - - diff --git a/services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee b/services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee deleted file mode 100644 index 81807baf70..0000000000 --- a/services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee +++ /dev/null @@ -1,69 +0,0 @@ -_ = require("lodash") -async = require("async") -settings = require("settings-sharelatex") - -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)-> - - sizeLimit = 1000 * 1000 * settings.compileBodySizeLimitMb - - 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 > sizeLimit - if !tooLarge - return callback() - else - sizedResources = _.sortBy(sizedResources, "size").reverse().slice(0, 10) - return callback(null, {resources:sizedResources, totalSize:totalSize}) - - - - - - - diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee deleted file mode 100755 index b0f7a01fa1..0000000000 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ /dev/null @@ -1,344 +0,0 @@ -Path = require "path" -async = require "async" -Settings = require "settings-sharelatex" -request = require('request') -Project = require("../../models/Project").Project -ProjectGetter = require("../Project/ProjectGetter") -ProjectEntityHandler = require("../Project/ProjectEntityHandler") -logger = require "logger-sharelatex" -Url = require("url") -ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) -NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi_new?.backendGroupName) -ClsiStateManager = require("./ClsiStateManager") -_ = require("underscore") -async = require("async") -ClsiFormatChecker = require("./ClsiFormatChecker") -DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" -Metrics = require('metrics-sharelatex') -Errors = require ('../Errors/Errors') - -module.exports = ClsiManager = - - sendRequest: (project_id, user_id, options = {}, callback) -> - ClsiManager.sendRequestOnce project_id, user_id, options, (error, status, result...) -> - return callback(error) if error? - if status is 'conflict' - options = _.clone(options) - options.syncType = "full" # force full compile - ClsiManager.sendRequestOnce project_id, user_id, options, callback # try again - else - callback(error, status, result...) - - sendRequestOnce: (project_id, user_id, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) -> - ClsiManager._buildRequest project_id, options, (error, req) -> - if error? - if error.message is "no main file specified" - return callback(null, "validation-problems", null, null, {mainFile:error.message}) - else - return callback(error) - logger.log project_id: project_id, "sending compile to CLSI" - ClsiManager._sendBuiltRequest project_id, user_id, req, options, (error, status, result...) -> - return callback(error) if error? - callback(error, status, result...) - - # for public API requests where there is no project id - sendExternalRequest: (submission_id, clsi_request, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) -> - logger.log submission_id: submission_id, "sending external compile to CLSI", clsi_request - ClsiManager._sendBuiltRequest submission_id, null, clsi_request, options, (error, status, result...) -> - return callback(error) if error? - callback(error, status, result...) - - stopCompile: (project_id, user_id, options, callback = (error) ->) -> - compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop") - opts = - 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, (clsiError) -> - # always clear the project state from the docupdater, even if there - # was a problem with the request to the clsi - DocumentUpdaterHandler.clearProjectState project_id, (docUpdaterError) -> - error = clsiError or docUpdaterError - return callback(error) if error? - callback() - - _sendBuiltRequest: (project_id, user_id, req, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) -> - 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) - logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI" - ClsiCookieManager._getServerId project_id, (err, clsiServerId)-> - if err? - logger.err err:err, project_id:project_id, "error getting server id" - return callback(err) - outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles) - callback(null, response?.compile?.status, outputFiles, clsiServerId) - - _makeRequest: (project_id, opts, callback)-> - async.series { - currentBackend: (cb)-> - startTime = new Date() - ClsiCookieManager.getCookieJar project_id, (err, jar)-> - if err? - logger.err err:err, "error getting cookie jar for clsi request" - return callback(err) - opts.jar = jar - timer = new Metrics.Timer("compile.currentBackend") - request opts, (err, response, body)-> - timer.done() - Metrics.inc "compile.currentBackend.response.#{response?.statusCode}" - if err? - logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi" - return callback(err) - ClsiCookieManager.setServerId project_id, response, (err)-> - if err? - logger.warn err:err, project_id:project_id, "error setting server id" - callback err, response, body #return as soon as the standard compile has returned - cb(err, {response:response, body:body, finishTime:new Date() - startTime }) - newBackend: (cb)-> - startTime = new Date() - ClsiManager._makeNewBackendRequest project_id, opts, (err, response, body)-> - Metrics.inc "compile.newBackend.response.#{response?.statusCode}" - cb(err, {response:response, body:body, finishTime:new Date() - startTime}) - }, (err, results)-> - timeDifference = results.newBackend?.finishTime - results.currentBackend?.finishTime - statusCodeSame = results.newBackend?.response?.statusCode == results.currentBackend?.response?.statusCode - currentCompileTime = results.currentBackend?.finishTime - newBackendCompileTime = results.newBackend?.finishTime - logger.log {statusCodeSame, timeDifference, currentCompileTime, newBackendCompileTime, project_id}, "both clsi requests returned" - - - - _makeNewBackendRequest: (project_id, baseOpts, callback)-> - if !Settings.apis.clsi_new?.url? - return callback() - opts = _.clone(baseOpts) - opts.url = opts.url.replace(Settings.apis.clsi.url, Settings.apis.clsi_new?.url) - NewBackendCloudClsiCookieManager.getCookieJar project_id, (err, jar)-> - if err? - logger.err err:err, "error getting cookie jar for clsi request" - return callback(err) - opts.jar = jar - timer = new Metrics.Timer("compile.newBackend") - request opts, (err, response, body)-> - timer.done() - if err? - logger.warn err:err, project_id:project_id, url:opts?.url, "error making request to new clsi" - return callback(err) - NewBackendCloudClsiCookieManager.setServerId project_id, response, (err)-> - if err? - logger.warn err:err, project_id:project_id, "error setting server id new backend" - 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}" - - _postToClsi: (project_id, user_id, req, compileGroup, callback = (error, response) ->) -> - compileUrl = @_getCompilerUrl(compileGroup, project_id, user_id, "compile") - opts = - url: compileUrl - json: req - method: "POST" - ClsiManager._makeRequest project_id, opts, (error, response, body) -> - return callback(error) if error? - if 200 <= response.statusCode < 300 - callback null, body - else if response.statusCode == 413 - callback null, compile:status:"project-too-large" - else if response.statusCode == 409 - callback null, compile:status:"conflict" - else if response.statusCode == 423 - callback null, compile:status:"compile-in-progress" - else - error = new Error("CLSI returned non-success code: #{response.statusCode}") - logger.error err: error, project_id: project_id, "CLSI returned failure code" - callback error, body - - _parseOutputFiles: (project_id, rawOutputFiles = []) -> - outputFiles = [] - for file in rawOutputFiles - outputFiles.push - 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 - - VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"] - - _buildRequest: (project_id, options={}, callback = (error, request) ->) -> - ProjectGetter.getProject project_id, {compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder:1}, (error, project) -> - return callback(error) if error? - return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project? - if project.compiler not in ClsiManager.VALID_COMPILERS - project.compiler = "pdflatex" - - if options.incrementalCompilesEnabled or options.syncType? # new way, either incremental or full - timer = new Metrics.Timer("editor.compile-getdocs-redis") - ClsiManager.getContentFromDocUpdaterIfMatch project_id, project, options, (error, projectStateHash, docUpdaterDocs) -> - timer.done() - if error? - logger.error err: error, project_id: project_id, "error checking project state" - # note: we don't bail out when there's an error getting - # incremental files from the docupdater, we just fall back - # to a normal compile below - else - logger.log project_id: project_id, projectStateHash: projectStateHash, docs: docUpdaterDocs?, "checked project state" - # see if we can send an incremental update to the CLSI - if docUpdaterDocs? and (options.syncType isnt "full") and not error? - Metrics.inc "compile-from-redis" - ClsiManager._buildRequestFromDocupdater project_id, options, project, projectStateHash, docUpdaterDocs, callback - else - Metrics.inc "compile-from-mongo" - ClsiManager._buildRequestFromMongo project_id, options, project, projectStateHash, callback - else # old way, always from mongo - timer = new Metrics.Timer("editor.compile-getdocs-mongo") - ClsiManager._getContentFromMongo project_id, (error, docs, files) -> - timer.done() - return callback(error) if error? - ClsiManager._finaliseRequest project_id, options, project, docs, files, callback - - getContentFromDocUpdaterIfMatch: (project_id, project, options, callback = (error, projectStateHash, docs) ->) -> - ClsiStateManager.computeHash project, options, (error, projectStateHash) -> - return callback(error) if error? - DocumentUpdaterHandler.getProjectDocsIfMatch project_id, projectStateHash, (error, docs) -> - return callback(error) if error? - callback(null, projectStateHash, docs) - - getOutputFileStream: (project_id, user_id, build_id, output_file_path, callback=(err, readStream)->) -> - url = "#{Settings.apis.clsi.url}/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{output_file_path}" - ClsiCookieManager.getCookieJar project_id, (err, jar)-> - return callback(err) if err? - options = { url: url, method: "GET", timeout: 60 * 1000, jar : jar } - readStream = request(options) - callback(null, readStream) - - _buildRequestFromDocupdater: (project_id, options, project, projectStateHash, docUpdaterDocs, callback = (error, request) ->) -> - ProjectEntityHandler.getAllDocPathsFromProject project, (error, docPath) -> - return callback(error) if error? - docs = {} - for doc in docUpdaterDocs or [] - path = docPath[doc._id] - docs[path] = doc - # send new docs but not files as those are already on the clsi - options = _.clone(options) - options.syncType = "incremental" - options.syncState = projectStateHash - # create stub doc entries for any possible root docs, if not - # present in the docupdater. This allows finaliseRequest to - # identify the root doc. - possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id] - for rootDoc_id in possibleRootDocIds when rootDoc_id? and rootDoc_id of docPath - path = docPath[rootDoc_id] - docs[path] ?= {_id: rootDoc_id, path: path} - ClsiManager._finaliseRequest project_id, options, project, docs, [], callback - - _buildRequestFromMongo: (project_id, options, project, projectStateHash, callback = (error, request) ->) -> - ClsiManager._getContentFromMongo project_id, (error, docs, files) -> - return callback(error) if error? - options = _.clone(options) - options.syncType = "full" - options.syncState = projectStateHash - ClsiManager._finaliseRequest project_id, options, project, docs, files, callback - - _getContentFromMongo: (project_id, callback = (error, docs, files) ->) -> - DocumentUpdaterHandler.flushProjectToMongo project_id, (error) -> - return callback(error) if error? - ProjectEntityHandler.getAllDocs project_id, (error, docs = {}) -> - return callback(error) if error? - ProjectEntityHandler.getAllFiles project_id, (error, files = {}) -> - return callback(error) if error? - callback(null, docs, files) - - _finaliseRequest: (project_id, options, project, docs, files, callback = (error, params) -> ) -> - resources = [] - rootResourcePath = null - rootResourcePathOverride = null - hasMainFile = false - numberOfDocsInProject = 0 - - for path, doc of docs - path = path.replace(/^\//, "") # Remove leading / - numberOfDocsInProject++ - if doc.lines? # add doc to resources unless it is just a stub entry - resources.push - path: path - content: doc.lines.join("\n") - if project.rootDoc_id? and doc._id.toString() == project.rootDoc_id.toString() - rootResourcePath = path - if options.rootDoc_id? and doc._id.toString() == options.rootDoc_id.toString() - rootResourcePathOverride = path - if path is "main.tex" - hasMainFile = true - - rootResourcePath = rootResourcePathOverride if rootResourcePathOverride? - if !rootResourcePath? - if hasMainFile - logger.warn {project_id}, "no root document found, setting to main.tex" - rootResourcePath = "main.tex" - else if numberOfDocsInProject is 1 # only one file, must be the main document - for path, doc of docs - rootResourcePath = path.replace(/^\//, "") # Remove leading / - logger.warn {project_id, rootResourcePath: rootResourcePath}, "no root document found, single document in project" - else - return callback new Error("no main file specified") - - for path, file of files - path = path.replace(/^\//, "") # Remove leading / - resources.push - path: path - url: "#{Settings.apis.filestore.url}/project/#{project._id}/file/#{file._id}" - modified: file.created?.getTime() - - callback null, { - compile: - options: - compiler: project.compiler - timeout: options.timeout - imageName: project.imageName - draft: !!options.draft - check: options.check - syncType: options.syncType - syncState: options.syncState - rootResourcePath: rootResourcePath - resources: resources - } - - wordCount: (project_id, user_id, file, options, callback = (error, response) ->) -> - ClsiManager._buildRequest project_id, options, (error, req) -> - filename = file || req?.compile?.rootResourcePath - 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? - if 200 <= response.statusCode < 300 - callback null, body - else - error = new Error("CLSI returned non-success code: #{response.statusCode}") - logger.error err: error, project_id: project_id, "CLSI returned failure code" - callback error, body - diff --git a/services/web/app/coffee/Features/Compile/ClsiStateManager.coffee b/services/web/app/coffee/Features/Compile/ClsiStateManager.coffee deleted file mode 100644 index 9aff9c166d..0000000000 --- a/services/web/app/coffee/Features/Compile/ClsiStateManager.coffee +++ /dev/null @@ -1,35 +0,0 @@ -Settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -crypto = require "crypto" -ProjectEntityHandler = require "../Project/ProjectEntityHandler" - -# The "state" of a project is a hash of the relevant attributes in the -# project object in this case we only need the rootFolder. -# -# The idea is that it will change if any doc or file is -# created/renamed/deleted, and also if the content of any file (not -# doc) changes. -# -# When the hash changes the full set of files on the CLSI will need to -# be updated. If it doesn't change then we can overwrite changed docs -# in place on the clsi, getting them from the docupdater. -# -# The docupdater is responsible for setting the key in redis, and -# unsetting it if it removes any documents from the doc updater. - -buildState = (s) -> - return crypto.createHash('sha1').update(s, 'utf8').digest('hex') - -module.exports = ClsiStateManager = - - computeHash: (project, options, callback = (err, hash) ->) -> - ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> - fileList = ("#{f.file._id}:#{f.file.rev}:#{f.file.created}:#{f.path}" for f in files or []) - docList = ("#{d.doc._id}:#{d.path}" for d in docs or []) - sortedEntityList = [docList..., fileList...].sort() - # ignore the isAutoCompile options as it doesn't affect the - # output, but include all other options e.g. draft - optionsList = ("option #{key}:#{value}" for key, value of options or {} when not (key in ['isAutoCompile'])) - sortedOptionsList = optionsList.sort() - hash = buildState([sortedEntityList..., sortedOptionsList...].join("\n")) - callback(null, hash) diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee deleted file mode 100755 index 8e64e8a3cf..0000000000 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ /dev/null @@ -1,280 +0,0 @@ -Metrics = require "metrics-sharelatex" -ProjectGetter = require('../Project/ProjectGetter') -CompileManager = require("./CompileManager") -ClsiManager = require("./ClsiManager") -logger = require "logger-sharelatex" -request = require "request" -sanitize = require('sanitizer') -Settings = require "settings-sharelatex" -AuthenticationController = require "../Authentication/AuthenticationController" -UserGetter = require "../User/UserGetter" -RateLimiter = require("../../infrastructure/RateLimiter") -ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) -Path = require("path") - -module.exports = CompileController = - - compile: (req, res, next = (error) ->) -> - res.setTimeout(5 * 60 * 1000) - project_id = req.params.Project_id - isAutoCompile = !!req.query?.auto_compile - user_id = AuthenticationController.getLoggedInUserId req - options = { - isAutoCompile: isAutoCompile - } - if req.body?.rootDoc_id? - options.rootDoc_id = req.body.rootDoc_id - else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy - options.rootDoc_id = req.body.settingsOverride.rootDoc_id - if req.body?.compiler - options.compiler = req.body.compiler - if req.body?.draft - options.draft = req.body.draft - if req.body?.check in ['validate', 'error', 'silent'] - options.check = req.body.check - if req.body?.incrementalCompilesEnabled - options.incrementalCompilesEnabled = true - 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 { - status: status - outputFiles: outputFiles - compileGroup: limits?.compileGroup - clsiServerId:clsiServerId - validationProblems:validationProblems - pdfDownloadDomain: Settings.pdfDownloadDomain - } - - stopCompile: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - user_id = AuthenticationController.getLoggedInUserId req - 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() - - # Used for submissions through the public API - compileSubmission: (req, res, next = (error) ->) -> - res.setTimeout(5 * 60 * 1000) - submission_id = req.params.submission_id - options = {} - if req.body?.rootResourcePath? - options.rootResourcePath = req.body.rootResourcePath - if req.body?.compiler - options.compiler = req.body.compiler - if req.body?.draft - options.draft = req.body.draft - if req.body?.check in ['validate', 'error', 'silent'] - options.check = req.body.check - options.compileGroup = req.body?.compileGroup || Settings.defaultFeatures.compileGroup - options.timeout = req.body?.timeout || Settings.defaultFeatures.compileTimeout - logger.log {options:options, submission_id:submission_id}, "got compileSubmission request" - ClsiManager.sendExternalRequest submission_id, req.body, options, (error, status, outputFiles, clsiServerId, validationProblems) -> - return next(error) if error? - logger.log {submission_id:submission_id, files:outputFiles}, "compileSubmission output files" - res.contentType("application/json") - res.status(200).send JSON.stringify { - status: status - outputFiles: outputFiles - clsiServerId: clsiServerId - validationProblems: validationProblems - } - - _compileAsUser: (req, callback) -> - # callback with user_id if per-user, undefined otherwise - if not Settings.disablePerUserCompiles - user_id = AuthenticationController.getLoggedInUserId req - return callback(null, 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 - user_id = AuthenticationController.getLoggedInUserId req - return callback(null, 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 - isPdfjsPartialDownload = req.query?.pdfng - rateLimit = (callback)-> - if isPdfjsPartialDownload - callback null, true - else - rateLimitOpts = - endpointName: "full-pdf-download" - throttle: 1000 - subjectName : req.ip - timeInterval : 60 * 60 - RateLimiter.addCount rateLimitOpts, callback - - ProjectGetter.getProject project_id, name: 1, (err, project) -> - res.contentType("application/pdf") - filename = "#{CompileController._getSafeProjectName(project)}.pdf" - - if !!req.query.popupDownload - logger.log project_id: project_id, "download pdf as popup download" - res.setContentDisposition('attachment', {filename}) - else - logger.log project_id: project_id, "download pdf to embed in browser" - res.setContentDisposition('', {filename}) - - rateLimit (err, canContinue)-> - if err? - logger.err err:err, "error checking rate limit for pdf download" - return res.send 500 - else if !canContinue - logger.log project_id:project_id, ip:req.ip, "rate limit hit downloading pdf" - res.send 500 - else - 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) - - _getSafeProjectName: (project) -> - safeProjectName = project.name.replace(new RegExp("\\W", "g"), '_') - sanitize.escape(safeProjectName) - - deleteAuxFiles: (req, res, next) -> - project_id = req.params.Project_id - 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" - res.sendStatus 500 - url = "/project/#{project_id}/output/output.pdf" - CompileController.proxyToClsi project_id, url, req, res, next - - getFileFromClsi: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - 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) - - getFileFromClsiWithoutUser: (req, res, next = (error) ->) -> - submission_id = req.params.submission_id - url = CompileController._getFileUrl submission_id, null, req.params.build_id, req.params.file - limits = { compileGroup: req.body?.compileGroup || Settings.defaultFeatures.compileGroup } - CompileController.proxyToClsiWithLimits(submission_id, url, limits, 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 - if not page?.match(/^\d+$/) - return next(new Error("invalid page parameter")) - if not h?.match(/^-?\d+\.\d+$/) - return next(new Error("invalid h parameter")) - if not v?.match(/^-?\d+\.\d+$/) - return next(new Error("invalid v parameter")) - # 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? - 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")) - 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) ->) -> - if req.query?.compileGroup - CompileController.proxyToClsiWithLimits(project_id, url, {compileGroup: req.query.compileGroup}, req, res, next) - else - CompileManager.getProjectCompileLimits project_id, (error, limits) -> - return next(error) if error? - CompileController.proxyToClsiWithLimits(project_id, url, limits, req, res, next) - - proxyToClsiWithLimits: (project_id, url, limits, req, res, next = (error) ->) -> - ClsiCookieManager.getCookieJar project_id, (err, jar)-> - if err? - logger.err err:err, "error getting cookie jar for clsi request" - return callback(err) - # expand any url parameter passed in as {url:..., qs:...} - if typeof url is "object" - {url, qs} = url - compilerUrl = Settings.apis.clsi.url - url = "#{compilerUrl}#{url}" - logger.log url: url, "proxying to CLSI" - oneMinute = 60 * 1000 - # the base request - options = { url: url, method: req.method, timeout: oneMinute, jar : jar } - # add any provided query string - options.qs = qs if qs? - # if we have a build parameter, pass it through to the clsi - if req.query?.pdfng && req.query?.build? # only for new pdf viewer - options.qs ?= {} - options.qs.build = req.query.build - # if we are byte serving pdfs, pass through If-* and Range headers - # do not send any others, there's a proxying loop if Host: is passed! - if req.query?.pdfng - newHeaders = {} - for h, v of req.headers - newHeaders[h] = req.headers[h] if /^(If-|Range)/i.test(h) - options.headers = newHeaders - proxy = request(options) - proxy.pipe(res) - proxy.on "error", (error) -> - logger.warn err: error, url: url, "CLSI proxy error" - - wordCount: (req, res, next) -> - project_id = req.params.Project_id - file = req.query.file || false - 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 diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee deleted file mode 100755 index 5e76104273..0000000000 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ /dev/null @@ -1,109 +0,0 @@ -Settings = require('settings-sharelatex') -RedisWrapper = require("../../infrastructure/RedisWrapper") -rclient = RedisWrapper.client("clsi_recently_compiled") -ProjectGetter = require('../Project/ProjectGetter') -ProjectRootDocManager = require "../Project/ProjectRootDocManager" -UserGetter = require "../User/UserGetter" -ClsiManager = require "./ClsiManager" -Metrics = require('metrics-sharelatex') -logger = require("logger-sharelatex") -rateLimiter = require("../../infrastructure/RateLimiter") - -module.exports = CompileManager = - - - compile: (project_id, user_id, options = {}, _callback = (error) ->) -> - timer = new Metrics.Timer("editor.compile") - callback = (args...) -> - timer.done() - _callback(args...) - - logger.log project_id: project_id, user_id: user_id, "compiling project" - CompileManager._checkIfRecentlyCompiled project_id, user_id, (error, recentlyCompiled) -> - return callback(error) if error? - if recentlyCompiled - logger.warn {project_id, user_id}, "project was recently compiled so not continuing" - return callback null, "too-recently-compiled", [] - - CompileManager._checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, "everyone", (err, canCompile)-> - if !canCompile - return callback null, "autocompile-backoff", [] - - ProjectRootDocManager.ensureRootDocumentIsSet project_id, (error) -> - return callback(error) if error? - CompileManager.getProjectCompileLimits project_id, (error, limits) -> - return callback(error) if error? - for key, value of limits - options[key] = value - # Put a lower limit on autocompiles for free users, based on compileGroup - CompileManager._checkCompileGroupAutoCompileLimit options.isAutoCompile, limits.compileGroup, (err, canCompile)-> - if !canCompile - return callback null, "autocompile-backoff", [] - # 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, validationProblems) - - - stopCompile: (project_id, user_id, callback = (error) ->) -> - CompileManager.getProjectCompileLimits project_id, (error, limits) -> - return callback(error) if error? - 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) ->) -> - ProjectGetter.getProject project_id, owner_ref: 1, (error, project) -> - return callback(error) if error? - UserGetter.getUser project.owner_ref, {"features":1}, (err, owner)-> - return callback(error) if error? - callback null, { - timeout: owner?.features?.compileTimeout || Settings.defaultFeatures.compileTimeout - compileGroup: owner?.features?.compileGroup || Settings.defaultFeatures.compileGroup - } - - COMPILE_DELAY: 1 # seconds - _checkIfRecentlyCompiled: (project_id, user_id, callback = (error, recentlyCompiled) ->) -> - key = "compile:#{project_id}:#{user_id}" - rclient.set key, true, "EX", @COMPILE_DELAY, "NX", (error, ok) -> - return callback(error) if error? - if ok == "OK" - return callback null, false - else - return callback null, true - - _checkCompileGroupAutoCompileLimit: (isAutoCompile, compileGroup, callback = (err, canCompile)->)-> - if !isAutoCompile - return callback(null, true) - if compileGroup is "standard" - # apply extra limits to the standard compile group - CompileManager._checkIfAutoCompileLimitHasBeenHit isAutoCompile, compileGroup, callback - else - Metrics.inc "auto-compile-#{compileGroup}" - return callback(null, true) # always allow priority group users to compile - - _checkIfAutoCompileLimitHasBeenHit: (isAutoCompile, compileGroup, callback = (err, canCompile)->)-> - if !isAutoCompile - return callback(null, true) - Metrics.inc "auto-compile-#{compileGroup}" - opts = - endpointName:"auto_compile" - timeInterval:20 - subjectName:compileGroup - throttle: Settings?.rateLimit?.autoCompile?[compileGroup] || 25 - rateLimiter.addCount opts, (err, canCompile)-> - if err? - canCompile = false - if !canCompile - Metrics.inc "auto-compile-#{compileGroup}-limited" - callback err, canCompile - - wordCount: (project_id, user_id, file, callback = (error) ->) -> - CompileManager.getProjectCompileLimits project_id, (error, limits) -> - return callback(error) if error? - ClsiManager.wordCount project_id, user_id, file, limits, callback diff --git a/services/web/app/coffee/Features/Contacts/ContactController.coffee b/services/web/app/coffee/Features/Contacts/ContactController.coffee deleted file mode 100644 index c2df6cc989..0000000000 --- a/services/web/app/coffee/Features/Contacts/ContactController.coffee +++ /dev/null @@ -1,42 +0,0 @@ -AuthenticationController = require "../Authentication/AuthenticationController" -ContactManager = require "./ContactManager" -UserGetter = require "../User/UserGetter" -logger = require "logger-sharelatex" -Modules = require "../../infrastructure/Modules" - -module.exports = ContactsController = - getContacts: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId req - ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) -> - return next(error) if error? - UserGetter.getUsers contact_ids, { - email: 1, first_name: 1, last_name: 1, holdingAccount: 1 - }, (error, contacts) -> - return next(error) if error? - - # UserGetter.getUsers may not preserve order so put them back in order - positions = {} - for contact_id, i in contact_ids - positions[contact_id] = i - contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()] - - # Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc) - contacts = contacts.filter (c) -> !c.holdingAccount - - contacts = contacts.map(ContactsController._formatContact) - - Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) -> - return next(error) if error? - contacts = contacts.concat(additional_contacts...) - res.send({ - contacts: contacts - }) - - _formatContact: (contact) -> - return { - id: contact._id?.toString() - email: contact.email || "" - first_name: contact.first_name || "" - last_name: contact.last_name || "" - type: "user" - } diff --git a/services/web/app/coffee/Features/Contacts/ContactManager.coffee b/services/web/app/coffee/Features/Contacts/ContactManager.coffee deleted file mode 100644 index 8f08b5ea41..0000000000 --- a/services/web/app/coffee/Features/Contacts/ContactManager.coffee +++ /dev/null @@ -1,39 +0,0 @@ -request = require "request" -settings = require "settings-sharelatex" -logger = require "logger-sharelatex" - -module.exports = ContactManager = - getContactIds: (user_id, options = { limits: 50 }, callback = (error, contacts) ->) -> - logger.log {user_id}, "getting user contacts" - url = "#{settings.apis.contacts.url}/user/#{user_id}/contacts" - request.get { - url: url - qs: options - json: true - jar: false - }, (error, res, data) -> - return callback(error) if error? - if 200 <= res.statusCode < 300 - callback(null, data?.contact_ids or []) - else - error = new Error("contacts api responded with non-success code: #{res.statusCode}") - logger.error {err: error, user_id}, "error getting contacts for user" - callback(error) - - addContact: (user_id, contact_id, callback = (error) ->) -> - logger.log {user_id, contact_id}, "add user contact" - url = "#{settings.apis.contacts.url}/user/#{user_id}/contacts" - request.post { - url: url - json: { - contact_id: contact_id - } - jar: false - }, (error, res, data) -> - return callback(error) if error? - if 200 <= res.statusCode < 300 - callback(null, data?.contact_ids or []) - else - error = new Error("contacts api responded with non-success code: #{res.statusCode}") - logger.error {err: error, user_id, contact_id}, "error adding contact for user" - callback(error) \ No newline at end of file diff --git a/services/web/app/coffee/Features/Contacts/ContactRouter.coffee b/services/web/app/coffee/Features/Contacts/ContactRouter.coffee deleted file mode 100644 index 211a83c18f..0000000000 --- a/services/web/app/coffee/Features/Contacts/ContactRouter.coffee +++ /dev/null @@ -1,9 +0,0 @@ -AuthenticationController = require('../Authentication/AuthenticationController') -ContactController = require "./ContactController" - -module.exports = - apply: (webRouter, apiRouter) -> - webRouter.get '/user/contacts', - AuthenticationController.requireLogin(), - ContactController.getContacts - diff --git a/services/web/app/coffee/Features/Cooldown/CooldownManager.coffee b/services/web/app/coffee/Features/Cooldown/CooldownManager.coffee deleted file mode 100644 index 9797cbf04d..0000000000 --- a/services/web/app/coffee/Features/Cooldown/CooldownManager.coffee +++ /dev/null @@ -1,23 +0,0 @@ -RedisWrapper = require('../../infrastructure/RedisWrapper') -rclient = RedisWrapper.client('cooldown') -logger = require('logger-sharelatex') - - -COOLDOWN_IN_SECONDS = 60 * 10 - - -module.exports = CooldownManager = - - _buildKey: (projectId) -> - "Cooldown:{#{projectId}}" - - putProjectOnCooldown: (projectId, callback=(err)->) -> - logger.log {projectId}, "[Cooldown] putting project on cooldown for #{COOLDOWN_IN_SECONDS} seconds" - rclient.set(CooldownManager._buildKey(projectId), '1', 'EX', COOLDOWN_IN_SECONDS, callback) - - isProjectOnCooldown: (projectId, callback=(err, isOnCooldown)->) -> - rclient.get CooldownManager._buildKey(projectId), (err, result) -> - if err? - return callback(err) - callback(null, result == "1") - diff --git a/services/web/app/coffee/Features/Cooldown/CooldownMiddleware.coffee b/services/web/app/coffee/Features/Cooldown/CooldownMiddleware.coffee deleted file mode 100644 index ee8edd6def..0000000000 --- a/services/web/app/coffee/Features/Cooldown/CooldownMiddleware.coffee +++ /dev/null @@ -1,17 +0,0 @@ -CooldownManager = require('./CooldownManager') -logger = require('logger-sharelatex') - - -module.exports = CooldownMiddleware = - - freezeProject: (req, res, next) -> - projectId = req.params.Project_id - if !projectId? - return next(new Error('[Cooldown] No projectId parameter on route')) - CooldownManager.isProjectOnCooldown projectId, (err, projectIsOnCooldown) -> - if err? - return next(err) - if projectIsOnCooldown - logger.log {projectId}, "[Cooldown] project is on cooldown, denying request" - return res.sendStatus(429) - next() diff --git a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee deleted file mode 100644 index 927121a6a1..0000000000 --- a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee +++ /dev/null @@ -1,123 +0,0 @@ -request = require("request").defaults(jar: false) -logger = require "logger-sharelatex" -settings = require "settings-sharelatex" -Errors = require "../Errors/Errors" - -module.exports = DocstoreManager = - deleteDoc: (project_id, doc_id, callback = (error) ->) -> - logger.log project_id: project_id, doc_id: doc_id, "deleting doc in docstore api" - url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}" - request.del url, (error, res, body) -> - return callback(error) if error? - if 200 <= res.statusCode < 300 - callback(null) - else if res.statusCode is 404 - error = new Errors.NotFoundError("tried to delete doc not in docstore") - logger.error err: error, project_id: project_id, doc_id: doc_id, "tried to delete doc not in docstore" - callback(error) # maybe suppress the error when delete doc which is not present? - else - error = new Error("docstore api responded with non-success code: #{res.statusCode}") - logger.error err: error, project_id: project_id, doc_id: doc_id, "error deleting doc in docstore" - callback(error) - - getAllDocs: (project_id, callback = (error) ->) -> - logger.log project_id: project_id, "getting all docs for project in docstore api" - url = "#{settings.apis.docstore.url}/project/#{project_id}/doc" - request.get { - url: url - json: true - }, (error, res, docs) -> - return callback(error) if error? - if 200 <= res.statusCode < 300 - callback(null, docs) - else - error = new Error("docstore api responded with non-success code: #{res.statusCode}") - logger.error err: error, project_id: project_id, "error getting all docs from docstore" - callback(error) - - getAllRanges: (project_id, callback = (error) ->) -> - logger.log { project_id }, "getting all doc ranges for project in docstore api" - url = "#{settings.apis.docstore.url}/project/#{project_id}/ranges" - request.get { - url: url - json: true - }, (error, res, docs) -> - return callback(error) if error? - if 200 <= res.statusCode < 300 - callback(null, docs) - else - error = new Error("docstore api responded with non-success code: #{res.statusCode}") - logger.error err: error, project_id: project_id, "error getting all doc ranges from docstore" - callback(error) - - getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) -> - if typeof(options) == "function" - callback = options - options = {} - logger.log project_id: project_id, doc_id: doc_id, options: options, "getting doc in docstore api" - url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}" - if options.include_deleted - url += "?include_deleted=true" - request.get { - url: url - json: true - }, (error, res, doc) -> - return callback(error) if error? - if 200 <= res.statusCode < 300 - logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api" - callback(null, doc.lines, doc.rev, doc.version, doc.ranges) - else if res.statusCode is 404 - error = new Errors.NotFoundError("doc not found in docstore") - logger.error err: error, project_id: project_id, doc_id: doc_id, "doc not found in docstore" - callback(error) - else - error = new Error("docstore api responded with non-success code: #{res.statusCode}") - logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore" - callback(error) - - updateDoc: (project_id, doc_id, lines, version, ranges, callback = (error, modified, rev) ->) -> - logger.log project_id: project_id, doc_id: doc_id, "updating doc in docstore api" - url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}" - request.post { - url: url - json: - lines: lines - version: version - ranges: ranges - }, (error, res, result) -> - return callback(error) if error? - if 200 <= res.statusCode < 300 - logger.log project_id: project_id, doc_id: doc_id, "update doc in docstore url finished" - callback(null, result.modified, result.rev) - else - error = new Error("docstore api responded with non-success code: #{res.statusCode}") - logger.error err: error, project_id: project_id, doc_id: doc_id, "error updating doc in docstore" - callback(error) - - archiveProject: (project_id, callback)-> - url = "#{settings.apis.docstore.url}/project/#{project_id}/archive" - logger.log project_id:project_id, "archiving project in docstore" - request.post url, (err, res, docs) -> - if err? - logger.err err:err, project_id:project_id, "error archving project in docstore" - return callback(err) - if 200 <= res.statusCode < 300 - callback() - else - error = new Error("docstore api responded with non-success code: #{res.statusCode}") - logger.err err: error, project_id: project_id, "error archiving project in docstore" - return callback(error) - - unarchiveProject: (project_id, callback)-> - url = "#{settings.apis.docstore.url}/project/#{project_id}/unarchive" - logger.log project_id:project_id, "unarchiving project in docstore" - request.post url, (err, res, docs) -> - if err? - logger.err err:err, project_id:project_id, "error unarchiving project in docstore" - return callback(err) - if 200 <= res.statusCode < 300 - callback() - else - error = new Error("docstore api responded with non-success code: #{res.statusCode}") - logger.err err: error, project_id: project_id, "error unarchiving project in docstore" - return callback(error) \ No newline at end of file diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee deleted file mode 100644 index 80206a2a1c..0000000000 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ /dev/null @@ -1,231 +0,0 @@ -request = require 'request' -request = request.defaults() -settings = require 'settings-sharelatex' -_ = require 'underscore' -async = require 'async' -logger = require('logger-sharelatex') -metrics = require('metrics-sharelatex') -Project = require("../../models/Project").Project - -module.exports = DocumentUpdaterHandler = - flushProjectToMongo: (project_id, callback = (error) ->)-> - logger.log project_id:project_id, "flushing project from document updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/flush" - method: "POST" - }, project_id, "flushing.mongo.project", callback - - flushMultipleProjectsToMongo: (project_ids, callback = (error) ->) -> - jobs = [] - for project_id in project_ids - do (project_id) -> - jobs.push (callback) -> - DocumentUpdaterHandler.flushProjectToMongo project_id, callback - async.series jobs, callback - - flushProjectToMongoAndDelete: (project_id, callback = ()->) -> - timer = new metrics.Timer("delete.mongo.project") - url = "#{settings.apis.documentupdater.url}" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}" - method: "DELETE" - }, project_id, "flushing.mongo.project", callback - - flushDocToMongo: (project_id, doc_id, callback = (error) ->) -> - logger.log project_id:project_id, doc_id: doc_id, "flushing doc from document updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/doc/#{doc_id}/flush" - method: "POST" - }, project_id, "flushing.mongo.doc", callback - - deleteDoc : (project_id, doc_id, callback = ()->)-> - logger.log project_id:project_id, doc_id: doc_id, "deleting doc from document updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/doc/#{doc_id}" - method: "DELETE" - }, project_id, "delete.mongo.doc", callback - - getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) -> - logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}" - json: true - }, project_id, "get-document", (error, doc) -> - return callback(error) if error? - callback null, doc.lines, doc.version, doc.ranges, doc.ops - - setDocument : (project_id, doc_id, user_id, docLines, source, callback = (error) ->)-> - logger.log project_id:project_id, doc_id: doc_id, source: source, user_id: user_id, "setting doc in document updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/doc/#{doc_id}" - method: "POST" - json: - lines: docLines - source: source - user_id: user_id - }, project_id, "set-document", callback - - getProjectDocsIfMatch: (project_id, projectStateHash, callback = (error, docs) ->) -> - # If the project state hasn't changed, we can get all the latest - # docs from redis via the docupdater. Otherwise we will need to - # fall back to getting them from mongo. - timer = new metrics.Timer("get-project-docs") - url = "#{settings.apis.documentupdater.url}/project/#{project_id}/get_and_flush_if_old?state=#{projectStateHash}" - logger.log project_id:project_id, "getting project docs from document updater" - request.post url, (error, res, body)-> - timer.done() - if error? - logger.error err:error, url:url, project_id:project_id, "error getting project docs from doc updater" - return callback(error) - if res.statusCode is 409 # HTTP response code "409 Conflict" - # Docupdater has checked the projectStateHash and found that - # it has changed. This means that the docs currently in redis - # aren't the only change to the project and the full set of - # docs/files should be retreived from docstore/filestore - # instead. - return callback() - else if res.statusCode >= 200 and res.statusCode < 300 - logger.log project_id:project_id, "got project docs from document document updater" - try - docs = JSON.parse(body) - catch error - return callback(error) - callback null, docs - else - logger.error project_id:project_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}" - callback new Error("doc updater returned a non-success status code: #{res.statusCode}") - - clearProjectState: (project_id, callback = (error) ->) -> - logger.log project_id:project_id, "clearing project state from document updater" - - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/clearState" - method: "POST" - }, project_id, "clear-project-state", callback - - acceptChanges: (project_id, doc_id, change_ids = [], callback = (error) ->) -> - logger.log {project_id, doc_id }, "accepting #{ change_ids.length } changes" - - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/doc/#{doc_id}/change/accept" - json: - change_ids: change_ids - method: "POST" - }, project_id, "accept-changes", callback - - deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) -> - timer = new metrics.Timer("delete-thread") - logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}" - method: "DELETE" - }, project_id, "delete-thread", callback - - resyncProjectHistory: (project_id, projectHistoryId, docs, files, callback) -> - logger.info {project_id, docs, files}, "resyncing project history in doc updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}/history/resync" - json: { docs, files, projectHistoryId } - method: "POST" - }, project_id, "resync-project-history", callback - - updateProjectStructure: (project_id, projectHistoryId, userId, changes, callback = (error) ->)-> - return callback() if !settings.apis.project_history?.sendProjectStructureOps - - docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs) - fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles) - projectVersion = changes?.newProject?.version - - return callback() if (docUpdates.length + fileUpdates.length) < 1 - - if !projectVersion? - logger.error {project_id, changes, projectVersion}, "did not receive project version in changes" - return callback(new Error("did not receive project version in changes")) - - logger.log {project_id}, "updating project structure in doc updater" - DocumentUpdaterHandler._makeRequest { - path: "/project/#{project_id}" - json: { - docUpdates, - fileUpdates, - userId, - version: projectVersion - projectHistoryId - } - method: "POST" - }, project_id, "update-project-structure", callback - - _makeRequest: (options, project_id, metricsKey, callback) -> - timer = new metrics.Timer(metricsKey) - request { - url: "#{settings.apis.documentupdater.url}#{options.path}" - json: options.json - method: options.method || "GET" - }, (error, res, body)-> - timer.done() - if error? - logger.error {error, project_id}, "error making request to document updater" - callback error - else if res.statusCode >= 200 and res.statusCode < 300 - callback null, body - else - error = new Error("document updater returned a failure status code: #{res.statusCode}") - logger.error {error, project_id}, "document updater returned failure status code: #{res.statusCode}" - callback error - - _getUpdates: (entityType, oldEntities, newEntities) -> - oldEntities ||= [] - newEntities ||= [] - updates = [] - - oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString() - newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString() - - # Send deletes before adds (and renames) to keep a 1:1 mapping between - # paths and ids - # - # When a file is replaced, we first delete the old file and then add the - # new file. If the 'add' operation is sent to project history before the - # 'delete' then we would have two files with the same path at that point - # in time. - for id, oldEntity of oldEntitiesHash - newEntity = newEntitiesHash[id] - - if !newEntity? - # entity deleted - updates.push - id: id - pathname: oldEntity.path - newPathname: '' - - for id, newEntity of newEntitiesHash - oldEntity = oldEntitiesHash[id] - - if !oldEntity? - # entity added - updates.push - id: id - pathname: newEntity.path - docLines: newEntity.docLines - url: newEntity.url - hash: newEntity.file?.hash - else if newEntity.path != oldEntity.path - # entity renamed - updates.push - id: id - pathname: oldEntity.path - newPathname: newEntity.path - - updates - -PENDINGUPDATESKEY = "PendingUpdates" -DOCLINESKEY = "doclines" -DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates" - -keys = - pendingUpdates : (op) -> "#{PENDINGUPDATESKEY}:#{op.doc_id}" - docsWithPendingUpdates: DOCIDSWITHPENDINGUPDATES - docLines : (op) -> "#{DOCLINESKEY}:#{op.doc_id}" - combineProjectIdAndDocId: (project_id, doc_id) -> "#{project_id}:#{doc_id}" - - diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee deleted file mode 100644 index b687e74966..0000000000 --- a/services/web/app/coffee/Features/Documents/DocumentController.coffee +++ /dev/null @@ -1,48 +0,0 @@ -ProjectGetter = require "../Project/ProjectGetter" -ProjectLocator = require "../Project/ProjectLocator" -ProjectEntityHandler = require "../Project/ProjectEntityHandler" -ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" -logger = require("logger-sharelatex") - -module.exports = - getDocument: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - doc_id = req.params.doc_id - plain = req?.query?.plain == 'true' - logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)" - ProjectGetter.getProject project_id, rootFolder: true, overleaf: true, (error, project) -> - return next(error) if error? - return res.sendStatus(404) if !project? - ProjectLocator.findElement {project: project, element_id: doc_id, type: 'doc'}, (error, doc, path) -> - if error? - logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" - return next(error) - ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version, ranges) -> - if error? - logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding doc contents for getDocument" - return next(error) - if plain - res.type "text/plain" - res.send lines.join('\n') - - else - projectHistoryId = project?.overleaf?.history?.id - res.json { - lines: lines - version: version - ranges: ranges - pathname: path.fileSystem - projectHistoryId: projectHistoryId - } - - setDocument: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - doc_id = req.params.doc_id - {lines, version, ranges, lastUpdatedAt, lastUpdatedBy} = req.body - logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)" - ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, (error) -> - if error? - logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" - return next(error) - logger.log doc_id:doc_id, project_id:project_id, "finished receiving set document request from api (docupdater)" - res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Documents/DocumentHelper.coffee b/services/web/app/coffee/Features/Documents/DocumentHelper.coffee deleted file mode 100644 index 39cf812823..0000000000 --- a/services/web/app/coffee/Features/Documents/DocumentHelper.coffee +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = DocumentHelper = - getTitleFromTexContent: (content, maxContentToScan = 30000) -> - TITLE_WITH_CURLY_BRACES = /\\[tT]itle\*?\s*{([^}]+)}/ - TITLE_WITH_SQUARE_BRACES = /\\[tT]itle\s*\[([^\]]+)\]/ - for line in DocumentHelper._getLinesFromContent(content, maxContentToScan) - if match = line.match(TITLE_WITH_CURLY_BRACES) || line.match(TITLE_WITH_SQUARE_BRACES) - return DocumentHelper.detex(match[1]) - - return null - - contentHasDocumentclass: (content, maxContentToScan = 30000) -> - for line in DocumentHelper._getLinesFromContent(content, maxContentToScan) - # We've had problems with this regex locking up CPU. - # Previously /.*\\documentclass/ would totally lock up on lines of 500kb (data text files :() - # This regex will only look from the start of the line, including whitespace so will return quickly - # regardless of line length. - return true if line.match /^\s*\\documentclass/ - - return false - - detex: (string) -> - return string.replace(/\\LaTeX/g, 'LaTeX') - .replace(/\\TeX/g, 'TeX') - .replace(/\\TikZ/g, 'TikZ') - .replace(/\\BibTeX/g, 'BibTeX') - .replace(/\\\[[A-Za-z0-9. ]*\]/g, ' ') # line spacing - .replace(/\\(?:[a-zA-Z]+|.|)/g, '') - .replace(/{}|~/g, ' ') - .replace(/[${}]/g, '') - .replace(/ +/g, ' ') - .trim() - - _getLinesFromContent: (content, maxContentToScan) -> - return if typeof content is 'string' then content.substring(0, maxContentToScan).split("\n") else content diff --git a/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee b/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee deleted file mode 100644 index e73dc06eb2..0000000000 --- a/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee +++ /dev/null @@ -1,40 +0,0 @@ -logger = require "logger-sharelatex" -Metrics = require "metrics-sharelatex" -ProjectGetter = require('../Project/ProjectGetter') -ProjectZipStreamManager = require "./ProjectZipStreamManager" -DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" - -module.exports = ProjectDownloadsController = - downloadProject: (req, res, next) -> - project_id = req.params.Project_id - Metrics.inc "zip-downloads" - logger.log project_id: project_id, "downloading project" - DocumentUpdaterHandler.flushProjectToMongo project_id, (error)-> - return next(error) if error? - ProjectGetter.getProject project_id, name: true, (error, project) -> - return next(error) if error? - ProjectZipStreamManager.createZipStreamForProject project_id, (error, stream) -> - return next(error) if error? - res.setContentDisposition( - 'attachment', - {filename: "#{project.name}.zip"} - ) - res.contentType('application/zip') - stream.pipe(res) - - downloadMultipleProjects: (req, res, next) -> - project_ids = req.query.project_ids.split(",") - Metrics.inc "zip-downloads-multiple" - logger.log project_ids: project_ids, "downloading multiple projects" - DocumentUpdaterHandler.flushMultipleProjectsToMongo project_ids, (error) -> - return next(error) if error? - ProjectZipStreamManager.createZipStreamForMultipleProjects project_ids, (error, stream) -> - return next(error) if error? - res.setContentDisposition( - 'attachment', - {filename: "Overleaf Projects (#{project_ids.length} items).zip"} - ) - res.contentType('application/zip') - stream.pipe(res) - - diff --git a/services/web/app/coffee/Features/Downloads/ProjectZipStreamManager.coffee b/services/web/app/coffee/Features/Downloads/ProjectZipStreamManager.coffee deleted file mode 100644 index ed20d1953c..0000000000 --- a/services/web/app/coffee/Features/Downloads/ProjectZipStreamManager.coffee +++ /dev/null @@ -1,80 +0,0 @@ -archiver = require "archiver" -async = require "async" -logger = require "logger-sharelatex" -ProjectEntityHandler = require "../Project/ProjectEntityHandler" -ProjectGetter = require('../Project/ProjectGetter') -FileStoreHandler = require("../FileStore/FileStoreHandler") - -module.exports = ProjectZipStreamManager = - createZipStreamForMultipleProjects: (project_ids, callback = (error, stream) ->) -> - # We'll build up a zip file that contains multiple zip files - - archive = archiver("zip") - archive.on "error", (err)-> - logger.err err:err, project_ids:project_ids, "something went wrong building archive of project" - callback null, archive - - logger.log project_ids: project_ids, "creating zip stream of multiple projects" - - jobs = [] - for project_id in project_ids or [] - do (project_id) -> - jobs.push (callback) -> - ProjectGetter.getProject project_id, name: true, (error, project) -> - return callback(error) if error? - logger.log project_id: project_id, name: project.name, "appending project to zip stream" - ProjectZipStreamManager.createZipStreamForProject project_id, (error, stream) -> - return callback(error) if error? - archive.append stream, name: "#{project.name}.zip" - stream.on "end", () -> - logger.log project_id: project_id, name: project.name, "zip stream ended" - callback() - - async.series jobs, () -> - logger.log project_ids: project_ids, "finished creating zip stream of multiple projects" - archive.finalize() - - createZipStreamForProject: (project_id, callback = (error, stream) ->) -> - archive = archiver("zip") - # return stream immediately before we start adding things to it - archive.on "error", (err)-> - logger.err err:err, project_id:project_id, "something went wrong building archive of project" - callback(null, archive) - @addAllDocsToArchive project_id, archive, (error) => - if error? - logger.error err: error, project_id: project_id, "error adding docs to zip stream" - @addAllFilesToArchive project_id, archive, (error) => - if error? - logger.error err: error, project_id: project_id, "error adding files to zip stream" - archive.finalize() - - - addAllDocsToArchive: (project_id, archive, callback = (error) ->) -> - ProjectEntityHandler.getAllDocs project_id, (error, docs) -> - return callback(error) if error? - jobs = [] - for path, doc of docs - do (path, doc) -> - path = path.slice(1) if path[0] == "/" - jobs.push (callback) -> - logger.log project_id: project_id, "Adding doc" - archive.append doc.lines.join("\n"), name: path - callback() - async.series jobs, callback - - addAllFilesToArchive: (project_id, archive, callback = (error) ->) -> - ProjectEntityHandler.getAllFiles project_id, (error, files) -> - return callback(error) if error? - jobs = [] - for path, file of files - do (path, file) -> - jobs.push (callback) -> - FileStoreHandler.getFileStream project_id, file._id, {}, (error, stream) -> - if error? - logger.err err:error, project_id:project_id, file_id:file._id, "something went wrong adding file to zip archive" - return callback(err) - path = path.slice(1) if path[0] == "/" - archive.append stream, name: path - stream.on "end", () -> - callback() - async.parallelLimit jobs, 5, callback diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee deleted file mode 100644 index 701ea0085b..0000000000 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ /dev/null @@ -1,217 +0,0 @@ -logger = require('logger-sharelatex') -Metrics = require('metrics-sharelatex') -sanitize = require('sanitizer') -ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') -ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') -ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') -ProjectDeleter = require("../Project/ProjectDeleter") -DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') -EditorRealTimeController = require("./EditorRealTimeController") -async = require('async') -PublicAccessLevels = require("../Authorization/PublicAccessLevels") -_ = require('underscore') - -module.exports = EditorController = - addDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)-> - EditorController.addDocWithRanges(project_id, folder_id, docName, docLines, {}, source, user_id, callback) - - addDocWithRanges: (project_id, folder_id, docName, docLines, docRanges, source, user_id, callback = (error, doc)->)-> - docName = docName.trim() - logger.log {project_id, folder_id, docName, source}, "sending new doc to project" - Metrics.inc "editor.add-doc" - ProjectEntityUpdateHandler.addDocWithRanges project_id, folder_id, docName, docLines, docRanges, user_id, (err, doc, folder_id)=> - if err? - logger.err err:err, project_id:project_id, docName:docName, "error adding doc without lock" - return callback(err) - EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source) - callback(err, doc) - - addFile: (project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id, callback = (error, file)->)-> - fileName = fileName.trim() - logger.log {project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id}, "sending new file to project" - Metrics.inc "editor.add-file" - ProjectEntityUpdateHandler.addFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, fileRef, folder_id)=> - if err? - logger.err err:err, project_id:project_id, folder_id:folder_id, fileName:fileName, "error adding file without lock" - return callback(err) - EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef, source, linkedFileData) - callback(err, fileRef) - - upsertDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (err)->)-> - ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, docName, docLines, source, user_id, (err, doc, didAddNewDoc) -> - if didAddNewDoc - EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source) - callback err, doc - - upsertFile: (project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id, callback = (err, file) ->) -> - ProjectEntityUpdateHandler.upsertFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile) -> - return callback(err) if err? - if not didAddFile # replacement, so remove the existing file from the client - EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source - # now add the new file on the client - EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', folder_id, newFile, source, linkedFileData - callback null, newFile - - upsertDocWithPath: (project_id, elementPath, docLines, source, user_id, callback) -> - ProjectEntityUpdateHandler.upsertDocWithPath project_id, elementPath, docLines, source, user_id, (err, doc, didAddNewDoc, newFolders, lastFolder) -> - return callback(err) if err? - EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) -> - return callback(err) if err? - if didAddNewDoc - EditorRealTimeController.emitToRoom project_id, 'reciveNewDoc', lastFolder._id, doc, source - callback() - - upsertFileWithPath: (project_id, elementPath, fsPath, linkedFileData, source, user_id, callback) -> - ProjectEntityUpdateHandler.upsertFileWithPath project_id, elementPath, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile, newFolders, lastFolder) -> - return callback(err) if err? - EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) -> - return callback(err) if err? - if not didAddFile # replacement, so remove the existing file from the client - EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source - # now add the new file on the client - EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', lastFolder._id, newFile, source, linkedFileData - callback() - - addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)-> - folderName = folderName.trim() - logger.log {project_id, folder_id, folderName, source}, "sending new folder to project" - Metrics.inc "editor.add-folder" - ProjectEntityUpdateHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=> - if err? - logger.err {err, project_id, folder_id, folderName, source}, "could not add folder" - return callback(err) - EditorController._notifyProjectUsersOfNewFolder project_id, folder_id, folder, (err) -> - return callback(err) if err? - callback null, folder - - mkdirp : (project_id, path, callback = (error, newFolders, lastFolder)->)-> - logger.log project_id:project_id, path:path, "making directories if they don't exist" - ProjectEntityUpdateHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=> - if err? - logger.err err:err, project_id:project_id, path:path, "could not mkdirp" - return callback(err) - - EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) -> - return callback(err) if err? - callback null, newFolders, lastFolder - - deleteEntity : (project_id, entity_id, entityType, source, userId, callback = (error)->)-> - logger.log {project_id, entity_id, entityType, source}, "start delete process of entity" - Metrics.inc "editor.delete-entity" - ProjectEntityUpdateHandler.deleteEntity project_id, entity_id, entityType, userId, (err)-> - if err? - logger.err {err, project_id, entity_id, entityType}, "could not delete entity" - return callback(err) - logger.log {project_id, entity_id, entityType}, "telling users entity has been deleted" - EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source) - callback() - - deleteEntityWithPath: (project_id, path, source, user_id, callback) -> - ProjectEntityUpdateHandler.deleteEntityWithPath project_id, path, user_id, (err, entity_id) -> - return callback(err) if err? - EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source) - callback null, entity_id - - notifyUsersProjectHasBeenDeletedOrRenamed: (project_id, callback)-> - EditorRealTimeController.emitToRoom(project_id, 'projectRenamedOrDeletedByExternalSource') - callback() - - updateProjectDescription: (project_id, description, callback = ->)-> - logger.log project_id:project_id, description:description, "updating project description" - ProjectDetailsHandler.setProjectDescription project_id, description, (err)-> - if err? - logger.err err:err, project_id:project_id, description:description, "something went wrong setting the project description" - return callback(err) - EditorRealTimeController.emitToRoom(project_id, 'projectDescriptionUpdated', description) - callback() - - deleteProject: (project_id, callback)-> - Metrics.inc "editor.delete-project" - logger.log project_id:project_id, "recived message to delete project" - ProjectDeleter.deleteProject project_id, callback - - renameEntity: (project_id, entity_id, entityType, newName, userId, callback = (error) ->)-> - newName = sanitize.escape(newName) - Metrics.inc "editor.rename-entity" - logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project" - ProjectEntityUpdateHandler.renameEntity project_id, entity_id, entityType, newName, userId, (err) -> - if err? - logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, newName:newName, "error renaming entity" - return callback(err) - if newName.length > 0 - EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName - callback() - - moveEntity: (project_id, entity_id, folder_id, entityType, userId, callback = (error) ->)-> - Metrics.inc "editor.move-entity" - ProjectEntityUpdateHandler.moveEntity project_id, entity_id, folder_id, entityType, userId, (err) -> - if err? - logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity" - return callback(err) - EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id - callback() - - renameProject: (project_id, newName, callback = (err) ->) -> - ProjectDetailsHandler.renameProject project_id, newName, (err) -> - if err? - logger.err err:err, project_id:project_id, newName:newName, "error renaming project" - return callback(err) - EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', newName - callback() - - setCompiler : (project_id, compiler, callback = (err) ->) -> - ProjectOptionsHandler.setCompiler project_id, compiler, (err) -> - return callback(err) if err? - logger.log compiler:compiler, project_id:project_id, "setting compiler" - EditorRealTimeController.emitToRoom project_id, 'compilerUpdated', compiler - callback() - - setImageName : (project_id, imageName, callback = (err) ->) -> - ProjectOptionsHandler.setImageName project_id, imageName, (err) -> - return callback(err) if err? - logger.log imageName:imageName, project_id:project_id, "setting imageName" - EditorRealTimeController.emitToRoom project_id, 'imageNameUpdated', imageName - callback() - - setSpellCheckLanguage : (project_id, languageCode, callback = (err) ->) -> - ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err) -> - return callback(err) if err? - logger.log languageCode:languageCode, project_id:project_id, "setting languageCode for spell check" - EditorRealTimeController.emitToRoom project_id, 'spellCheckLanguageUpdated', languageCode - callback() - - setPublicAccessLevel : (project_id, newAccessLevel, callback = (err) ->) -> - ProjectDetailsHandler.setPublicAccessLevel project_id, newAccessLevel, (err) -> - return callback(err) if err? - EditorRealTimeController.emitToRoom( - project_id, - 'project:publicAccessLevel:changed', - {newAccessLevel} - ) - if newAccessLevel == PublicAccessLevels.TOKEN_BASED - ProjectDetailsHandler.ensureTokensArePresent project_id, (err, tokens) -> - return callback(err) if err? - EditorRealTimeController.emitToRoom( - project_id, - 'project:tokens:changed', - {tokens} - ) - callback() - else - callback() - - setRootDoc: (project_id, newRootDocID, callback = (err) ->) -> - ProjectEntityUpdateHandler.setRootDoc project_id, newRootDocID, (err) -> - return callback(err) if err? - EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID - callback() - - _notifyProjectUsersOfNewFolders: (project_id, folders, callback = (error)->)-> - async.eachSeries folders, - (folder, cb) -> EditorController._notifyProjectUsersOfNewFolder project_id, folder.parentFolder_id, folder, cb - callback - - _notifyProjectUsersOfNewFolder: (project_id, folder_id, folder, callback = (error)->)-> - logger.log project_id:project_id, folder:folder, parentFolder_id:folder_id, "sending newly created folder out to users" - EditorRealTimeController.emitToRoom(project_id, "reciveNewFolder", folder_id, folder) - callback() diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee deleted file mode 100644 index 743b9ac190..0000000000 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ /dev/null @@ -1,134 +0,0 @@ -ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" -ProjectDeleter = require "../Project/ProjectDeleter" -logger = require "logger-sharelatex" -EditorRealTimeController = require "./EditorRealTimeController" -EditorController = require "./EditorController" -ProjectGetter = require('../Project/ProjectGetter') -UserGetter = require('../User/UserGetter') -AuthorizationManager = require("../Authorization/AuthorizationManager") -ProjectEditorHandler = require('../Project/ProjectEditorHandler') -Metrics = require('metrics-sharelatex') -CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") -CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler") -PrivilegeLevels = require "../Authorization/PrivilegeLevels" -TokenAccessHandler = require '../TokenAccess/TokenAccessHandler' -AuthenticationController = require "../Authentication/AuthenticationController" - -module.exports = EditorHttpController = - joinProject: (req, res, next) -> - project_id = req.params.Project_id - user_id = req.query.user_id - if user_id == "anonymous-user" - user_id = null - logger.log {user_id, project_id}, "join project request" - Metrics.inc "editor.join-project" - EditorHttpController._buildJoinProjectView req, project_id, user_id, (error, project, privilegeLevel) -> - return next(error) if error? - # Hide access tokens if this is not the project owner - TokenAccessHandler.protectTokens(project, privilegeLevel) - res.json { - project: project - privilegeLevel: privilegeLevel - } - # Only show the 'renamed or deleted' message once - if project?.deletedByExternalDataSource - ProjectDeleter.unmarkAsDeletedByExternalSource project_id - - _buildJoinProjectView: (req, project_id, user_id, callback = (error, project, privilegeLevel) ->) -> - logger.log {project_id, user_id}, "building the joinProject view" - ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> - return callback(error) if error? - return callback(new Error("not found")) if !project? - CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels project_id, (error, members) -> - return callback(error) if error? - token = TokenAccessHandler.getRequestToken(req, project_id) - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) -> - return callback(error) if error? - if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE - logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null" - return callback null, null, false - CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> - return callback(error) if error? - logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view" - callback(null, - ProjectEditorHandler.buildProjectModelView(project, members, invites), - privilegeLevel - ) - - _nameIsAcceptableLength: (name)-> - return name? and name.length < 150 and name.length != 0 - - addDoc: (req, res, next) -> - project_id = req.params.Project_id - name = req.body.name - parent_folder_id = req.body.parent_folder_id - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log project_id:project_id, name:name, parent_folder_id:parent_folder_id, "getting request to add doc to project" - if !EditorHttpController._nameIsAcceptableLength(name) - return res.sendStatus 400 - EditorController.addDoc project_id, parent_folder_id, name, [], "editor", user_id, (error, doc) -> - if error == "project_has_to_many_files" - res.status(400).json(req.i18n.translate("project_has_to_many_files")) - else if error? - next(error) - else - res.json doc - - addFolder: (req, res, next) -> - project_id = req.params.Project_id - name = req.body.name - parent_folder_id = req.body.parent_folder_id - if !EditorHttpController._nameIsAcceptableLength(name) - return res.sendStatus 400 - EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) -> - if error == "project_has_to_many_files" - res.status(400).json(req.i18n.translate("project_has_to_many_files")) - else if error?.message == 'invalid element name' - res.status(400).json(req.i18n.translate('invalid_file_name')) - else if error? - next(error) - else - res.json doc - - renameEntity: (req, res, next) -> - project_id = req.params.Project_id - entity_id = req.params.entity_id - entity_type = req.params.entity_type - name = req.body.name - if !EditorHttpController._nameIsAcceptableLength(name) - return res.sendStatus 400 - user_id = AuthenticationController.getLoggedInUserId(req) - EditorController.renameEntity project_id, entity_id, entity_type, name, user_id, (error) -> - return next(error) if error? - res.sendStatus 204 - - moveEntity: (req, res, next) -> - project_id = req.params.Project_id - entity_id = req.params.entity_id - entity_type = req.params.entity_type - folder_id = req.body.folder_id - user_id = AuthenticationController.getLoggedInUserId(req) - EditorController.moveEntity project_id, entity_id, folder_id, entity_type, user_id, (error) -> - return next(error) if error? - res.sendStatus 204 - - deleteDoc: (req, res, next)-> - req.params.entity_type = "doc" - EditorHttpController.deleteEntity(req, res, next) - - deleteFile: (req, res, next)-> - req.params.entity_type = "file" - EditorHttpController.deleteEntity(req, res, next) - - deleteFolder: (req, res, next)-> - req.params.entity_type = "folder" - EditorHttpController.deleteEntity(req, res, next) - - deleteEntity: (req, res, next) -> - project_id = req.params.Project_id - entity_id = req.params.entity_id - entity_type = req.params.entity_type - user_id = AuthenticationController.getLoggedInUserId(req) - EditorController.deleteEntity project_id, entity_id, entity_type, "editor", user_id, (error) -> - return next(error) if error? - res.sendStatus 204 diff --git a/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee b/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee deleted file mode 100644 index 9d0a5e5084..0000000000 --- a/services/web/app/coffee/Features/Editor/EditorRealTimeController.coffee +++ /dev/null @@ -1,23 +0,0 @@ -Settings = require 'settings-sharelatex' -RedisWrapper = require("../../infrastructure/RedisWrapper") -rclient = RedisWrapper.client("realtime") -os = require "os" -crypto = require "crypto" - -HOST = os.hostname() -RND = crypto.randomBytes(4).toString('hex') # generate a random key for this process -COUNT = 0 - -module.exports = EditorRealTimeController = - emitToRoom: (room_id, message, payload...) -> - # create a unique message id using a counter - message_id = "web:#{HOST}:#{RND}-#{COUNT++}" - rclient.publish "editor-events", JSON.stringify - room_id: room_id - message: message - payload: payload - _id: message_id - - emitToAll: (message, payload...) -> - @emitToRoom "all", message, payload... - diff --git a/services/web/app/coffee/Features/Editor/EditorRouter.coffee b/services/web/app/coffee/Features/Editor/EditorRouter.coffee deleted file mode 100644 index bcc2bd6d9e..0000000000 --- a/services/web/app/coffee/Features/Editor/EditorRouter.coffee +++ /dev/null @@ -1,39 +0,0 @@ -EditorHttpController = require('./EditorHttpController') -AuthenticationController = require "../Authentication/AuthenticationController" -AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') -RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') - -module.exports = - apply: (webRouter, apiRouter) -> - webRouter.post '/project/:Project_id/doc', AuthorizationMiddleware.ensureUserCanWriteProjectContent, - RateLimiterMiddleware.rateLimit({ - endpointName: "add-doc-to-project" - params: ["Project_id"] - maxRequests: 30 - timeInterval: 60 - }), EditorHttpController.addDoc - webRouter.post '/project/:Project_id/folder', AuthorizationMiddleware.ensureUserCanWriteProjectContent, - RateLimiterMiddleware.rateLimit({ - endpointName: "add-folder-to-project" - params: ["Project_id"] - maxRequests: 60 - timeInterval: 60 - }), EditorHttpController.addFolder - - webRouter.post '/project/:Project_id/:entity_type/:entity_id/rename', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.renameEntity - webRouter.post '/project/:Project_id/:entity_type/:entity_id/move', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.moveEntity - - webRouter.delete '/project/:Project_id/file/:entity_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.deleteFile - webRouter.delete '/project/:Project_id/doc/:entity_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.deleteDoc - webRouter.delete '/project/:Project_id/folder/:entity_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.deleteFolder - - # Called by the real-time API to load up the current project state. - # This is a post request because it's more than just a getting of data. We take actions - # whenever a user joins a project, like updating the deleted status. - apiRouter.post '/project/:Project_id/join', AuthenticationController.httpAuth, - RateLimiterMiddleware.rateLimit({ - endpointName: "join-project" - params: ["Project_id"] - maxRequests: 30 - timeInterval: 60 - }), EditorHttpController.joinProject diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee deleted file mode 100644 index 1a136d7e8c..0000000000 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ /dev/null @@ -1,230 +0,0 @@ -_ = require('underscore') -settings = require("settings-sharelatex") -marked = require('marked') -StringHelper = require "../Helpers/StringHelper" - -PersonalEmailLayout = require("./Layouts/PersonalEmailLayout") -NotificationEmailLayout = require("./Layouts/NotificationEmailLayout") -BaseWithHeaderEmailLayout = require("./Layouts/" + settings.brandPrefix + "BaseWithHeaderEmailLayout") -SpamSafe = require("./SpamSafe") - -SingleCTAEmailBody = require("./Bodies/" + settings.brandPrefix + "SingleCTAEmailBody") - -CTAEmailTemplate = (content) -> - content.greeting ?= () -> 'Hi,' - content.secondaryMessage ?= () -> "" - return { - subject: (opts) -> content.subject(opts), - layout: BaseWithHeaderEmailLayout, - plainTextTemplate: (opts) -> """ -#{content.greeting(opts)} - -#{content.message(opts).trim()} - -#{content.ctaText(opts)}: #{content.ctaURL(opts)} - -#{content.secondaryMessage?(opts).trim() or ""} - -Regards, -The #{settings.appName} Team - #{settings.siteUrl} - """ - compiledTemplate: (opts) -> - SingleCTAEmailBody({ - title: content.title?(opts) - greeting: content.greeting(opts) - message: marked(content.message(opts).trim()) - secondaryMessage: marked(content.secondaryMessage(opts).trim()) - ctaText: content.ctaText(opts) - ctaURL: content.ctaURL(opts) - gmailGoToAction: content.gmailGoToAction?(opts) - StringHelper: StringHelper - }) - } - -templates = {} - -templates.accountMergeToOverleafAddress = CTAEmailTemplate({ - subject: () -> "Confirm Account Merge - #{settings.appName}" - title: () -> "Confirm Account Merge" - message: () -> - """ - To merge your ShareLaTeX and Overleaf accounts, click the button below. - If you think you have received this message in error, - please contact us at https://www.overleaf.com/contact - """ - ctaText: () -> "Confirm Account Merge" - ctaURL: (opts) -> opts.tokenLinkUrl -}) - -templates.accountMergeToSharelatexAddress = templates.accountMergeToOverleafAddress - -templates.registered = CTAEmailTemplate({ - subject: () -> "Activate your #{settings.appName} Account" - message: (opts) -> """ -Congratulations, you've just had an account created for you on #{settings.appName} with the email address '#{_.escape(opts.to)}'. - -Click here to set your password and log in: -""" - secondaryMessage: () -> "If you have any questions or problems, please contact #{settings.adminEmail}" - ctaText: () -> "Set password" - ctaURL: (opts) -> opts.setNewPasswordUrl -}) - -templates.canceledSubscription = CTAEmailTemplate({ - subject: () -> "#{settings.appName} thoughts" - message: () -> """ -I'm sorry to see you cancelled your #{settings.appName} premium account. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey? -""" - secondaryMessage: () -> "Thank you in advance!" - ctaText: () -> "Leave Feedback" - ctaURL: (opts) -> "https://docs.google.com/forms/d/e/1FAIpQLScqU6Je1r4Afz6ul6oY0RAfN7RabdWv_oL1u7Rj1YBmXS4fiQ/viewform?usp=sf_link" -}) - -templates.reactivatedSubscription = CTAEmailTemplate({ - subject: () -> "Subscription Reactivated - #{settings.appName}" - message: (opts) -> """ -Your subscription was reactivated successfully. -""" - ctaText: () -> "View Subscription Dashboard" - ctaURL: (opts) -> "#{settings.siteUrl}/user/subscription" -}) - -templates.passwordResetRequested = CTAEmailTemplate({ - subject: () -> "Password Reset - #{settings.appName}" - title: () -> "Password Reset" - message: () -> "We got a request to reset your #{settings.appName} password." - secondaryMessage: () -> """ -If you ignore this message, your password won't be changed. - -If you didn't request a password reset, let us know. -""" - ctaText: () -> "Reset password" - ctaURL: (opts) -> opts.setNewPasswordUrl -}) - -templates.confirmEmail = CTAEmailTemplate({ - subject: () -> "Confirm Email - #{settings.appName}" - title: () -> "Confirm Email" - message: () -> "Please confirm your email on #{settings.appName}." - ctaText: () -> "Confirm Email" - ctaURL: (opts) -> opts.confirmEmailUrl -}) - -templates.projectInvite = CTAEmailTemplate({ - subject: (opts) -> "#{ _.escape(SpamSafe.safeProjectName(opts.project.name, "New Project")) } - shared by #{ _.escape(SpamSafe.safeEmail(opts.owner.email, "a collaborator")) }" - title: (opts) -> "#{ _.escape(SpamSafe.safeProjectName(opts.project.name, "New Project")) } - shared by #{ _.escape(SpamSafe.safeEmail(opts.owner.email, "a collaborator")) }" - message: (opts) -> "#{ _.escape(SpamSafe.safeEmail(opts.owner.email, "a collaborator")) } wants to share #{ _.escape(SpamSafe.safeProjectName(opts.project.name, "a new project")) } with you." - ctaText: () -> "View project" - ctaURL: (opts) -> opts.inviteUrl - gmailGoToAction: (opts) -> - target: opts.inviteUrl - name: "View project" - description: "Join #{ _.escape(SpamSafe.safeProjectName(opts.project.name, "project")) } at #{ settings.appName }" -}) - -templates.verifyEmailToJoinTeam = CTAEmailTemplate({ - subject: (opts) -> "#{ _.escape(SpamSafe.safeUserName(opts.inviterName, "A collaborator")) } has invited you to join a team on #{ settings.appName }" - title: (opts) -> "#{ _.escape(SpamSafe.safeUserName(opts.inviterName, "A collaborator")) } has invited you to join a team on #{ settings.appName }" - message: (opts) -> "Please click the button below to join the team and enjoy the benefits of an upgraded #{ settings.appName } account." - ctaText: (opts) -> "Join now" - ctaURL: (opts) -> opts.acceptInviteUrl -}) - -templates.dropboxUnlinkedDuplicate = CTAEmailTemplate({ - subject: () -> "Your Dropbox Account has been Unlinked - #{settings.appName}" - message: (opts) -> """ -Our automated systems have detected that your Dropbox account was linked to more than one Overleaf accounts. This should not have been allowed and might be causing issues with the Dropbox sync feature. - -We have now unlinked all your Dropbox and Overleaf Accounts. To ensure your project will keep syncing you can link your Dropbox account to the Overleaf account of your choice now. -""" - ctaText: () -> "Link Dropbox Account" - ctaURL: (opts) -> "#{settings.siteUrl}/user/settings" -}) - -templates.testEmail = CTAEmailTemplate({ - subject: () -> "A Test Email from #{settings.appName}" - title: () -> "A Test Email from #{settings.appName}" - greeting: () -> "Hi," - message: () -> "This is a test Email from #{settings.appName}" - ctaText: () -> "Open #{settings.appName}" - ctaURL: () -> settings.siteUrl -}) - -templates.projectsTransferredFromSharelatex = CTAEmailTemplate({ - subject: () -> "ShareLaTeX projects transferred to your Overleaf account" - title: () -> "ShareLaTeX projects transferred to your Overleaf account" - message: (opts) -> """ -We are writing with important information about your Overleaf and ShareLaTeX accounts. - -As part of our ongoing work to [integrate Overleaf and ShareLaTeX](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf), -we found a ShareLaTeX account with the email address #{opts.to} that matches your Overleaf account. - -We have now transferred the projects from this ShareLaTeX account into your Overleaf account, so you may notice some new -projects on your Overleaf projects page. - -When you next log in, you may be prompted to reconfirm your email address in order to regain access to your account. -If you have any questions, please contact our support team by reply. -""" - ctaText: () -> "Log in to #{ settings.appName }" - ctaURL: () -> settings.siteUrl + "/login" -}) - -templates.emailAddressPoachedEmail = CTAEmailTemplate({ - subject: () -> "One of your email addresses has been moved to another #{ settings.appName } account" - title: () -> "One of your email addresses has been moved to another #{ settings.appName } account" - message: (opts) -> - message = """ -We are writing with important information about your Overleaf account. - -You added the email address #{opts.poached} to your #{opts.to} Overleaf account as a secondary (or affiliation) -email address, but we have had to remove it. - -This is because your #{opts.poached} email address was also in use as the primary email address for an older Overleaf -account from before our [integration with ShareLaTeX to create Overleaf v2](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf). - -### What do I need to do? - -You now have two Overleaf accounts, one under #{opts.poached} and one under #{opts.to}. - -You may wish to log in to Overleaf as #{opts.poached} to check whether you have projects there that you would like to -keep. If you are not sure of the password, you can send yourself a password reset email to #{opts.poached}, via -https://www.overleaf.com/user/password/reset - -Once you have downloaded your projects, you may wish to delete your -#{opts.poached} Overleaf account, which you can do from your account settings. You will then be able to add -#{opts.poached} as a secondary email address on your #{opts.to} account again. - - -""" - if opts.proFeatures - message += """ -Because your #{opts.poached} email address was an institutional affiliation through which you had Pro features. Your Pro -features have been transferred to your #{opts.poached} account. If you would like to transfer them back to your -#{opts.to} account, you will need to delete the #{opts.poached} account and re-add it as a secondary email address, -as described above. - - -""" - - message += """ -If you have any questions, you can contact our support team by reply. -""" - return message - ctaText: () -> "Log in to #{ settings.appName }" - ctaURL: () -> settings.siteUrl + "/login" -}) - -module.exports = - templates: templates - CTAEmailTemplate: CTAEmailTemplate - buildEmail: (templateName, opts)-> - 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) - text: template?.plainTextTemplate?(opts) - } diff --git a/services/web/app/coffee/Features/Email/EmailHandler.coffee b/services/web/app/coffee/Features/Email/EmailHandler.coffee deleted file mode 100644 index 048796b09a..0000000000 --- a/services/web/app/coffee/Features/Email/EmailHandler.coffee +++ /dev/null @@ -1,20 +0,0 @@ -settings = require("settings-sharelatex") -EmailBuilder = require "./EmailBuilder" -EmailSender = require "./EmailSender" - -if !settings.email? - settings.email = - lifecycleEnabled:false - - -module.exports = - - sendEmail : (emailType, opts, callback = (err)->)-> - email = EmailBuilder.buildEmail emailType, opts - if email.type == "lifecycle" and !settings.email.lifecycle - return callback() - opts.html = email.html - opts.text = email.text - opts.subject = email.subject - EmailSender.sendEmail opts, (err)-> - callback(err) diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee deleted file mode 100644 index d5ae949f74..0000000000 --- a/services/web/app/coffee/Features/Email/EmailSender.coffee +++ /dev/null @@ -1,79 +0,0 @@ -logger = require('logger-sharelatex') -metrics = require('metrics-sharelatex') -Settings = require('settings-sharelatex') -nodemailer = require("nodemailer") -sesTransport = require('nodemailer-ses-transport') -sgTransport = require('nodemailer-sendgrid-transport') -mandrillTransport = require('nodemailer-mandrill-transport') -rateLimiter = require('../../infrastructure/RateLimiter') -_ = require("underscore") - -if Settings.email? and Settings.email.fromAddress? - defaultFromAddress = Settings.email.fromAddress -else - defaultFromAddress = "" - -# provide dummy mailer unless we have a better one configured. -client = - sendMail: (options, callback = (err,status) ->) -> - logger.log options:options, "Would send email if enabled." - callback() -if Settings?.email?.parameters?.AWSAccessKeyID? or Settings?.email?.driver == 'ses' - logger.log "using aws ses for email" - nm_client = nodemailer.createTransport(sesTransport(Settings.email.parameters)) -else if Settings?.email?.parameters?.sendgridApiKey? - logger.log "using sendgrid for email" - nm_client = nodemailer.createTransport(sgTransport({auth:{api_key:Settings?.email?.parameters?.sendgridApiKey}})) -else if Settings?.email?.parameters?.MandrillApiKey? - logger.log "using mandril for email" - nm_client = nodemailer.createTransport(mandrillTransport({auth:{apiKey:Settings?.email?.parameters?.MandrillApiKey}})) -else if Settings?.email?.parameters? - logger.log "using smtp for email" - smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth", "ignoreTLS") - nm_client = nodemailer.createTransport(smtp) -else - logger.warn "Email transport and/or parameters not defined. No emails will be sent." - nm_client = client - -if nm_client? - client = nm_client -else - logger.warn "Failed to create email transport. Please check your settings. No email will be sent." - -checkCanSendEmail = (options, callback)-> - if !options.sendingUser_id? #email not sent from user, not rate limited - return callback(null, true) - opts = - endpointName: "send_email" - timeInterval: 60 * 60 * 3 - subjectName: options.sendingUser_id - throttle: 100 - rateLimiter.addCount opts, callback - -module.exports = - sendEmail : (options, callback = (error) ->)-> - logger.log receiver:options.to, subject:options.subject, "sending email" - checkCanSendEmail options, (err, canContinue)-> - if err? - return callback(err) - if !canContinue - logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending" - return callback("rate limit hit sending email") - metrics.inc "email" - options = - to: options.to - from: defaultFromAddress - subject: options.subject - html: options.html - text: options.text - replyTo: options.replyTo || Settings.email.replyToAddress - socketTimeout: 30 * 1000 - if Settings.email.textEncoding? - opts.textEncoding = textEncoding - client.sendMail options, (err, res)-> - if err? - logger.err err:err, "error sending message" - err = new Error('Cannot send email') - else - logger.log "Message sent to #{options.to}" - callback(err) diff --git a/services/web/app/coffee/Features/Email/SpamSafe.coffee b/services/web/app/coffee/Features/Email/SpamSafe.coffee deleted file mode 100644 index 26ba49bc15..0000000000 --- a/services/web/app/coffee/Features/Email/SpamSafe.coffee +++ /dev/null @@ -1,38 +0,0 @@ -XRegExp = require('xregexp') - -# A note about SAFE_REGEX: -# We have to escape the escape characters because XRegExp compiles it first. -# So it's equivalent to `^[\p{L}\p{N}\s\-_!&\(\)]+$] -# \p{L} = any letter in any language -# \p{N} = any kind of numeric character -# https://www.regular-expressions.info/unicode.html#prop is a good resource for -# more obscure regex features. standard RegExp does not support these - -SAFE_REGEX = XRegExp("^[\\p{L}\\p{N}\\s\\-_!'&\\(\\)]+$") -EMAIL_REGEX = XRegExp("^[\\p{L}\\p{N}.+_-]+@[\\w.-]+$") - -SpamSafe = - isSafeUserName: (name) -> - SAFE_REGEX.test(name) && name.length <= 30 - - isSafeProjectName: (name) -> - if XRegExp("\\p{Han}").test(name) - SAFE_REGEX.test(name) && name.length <= 30 - SAFE_REGEX.test(name) && name.length <= 100 - - isSafeEmail: (email) -> - EMAIL_REGEX.test(email) && email.length <= 40 - - safeUserName: (name, alternative, project = false) -> - return name if SpamSafe.isSafeUserName name - alternative - - safeProjectName: (name, alternative) -> - return name if SpamSafe.isSafeProjectName name - alternative - - safeEmail: (email, alternative) -> - return email if SpamSafe.isSafeEmail email - alternative - -module.exports = SpamSafe diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee deleted file mode 100644 index fa252a24b2..0000000000 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ /dev/null @@ -1,61 +0,0 @@ -Errors = require "./Errors" -logger = require "logger-sharelatex" -AuthenticationController = require '../Authentication/AuthenticationController' - -module.exports = ErrorController = - notFound: (req, res)-> - res.status(404) - res.render 'general/404', - title: "page_not_found" - - forbidden: (req, res) -> - res.status(403) - res.render 'user/restricted' - - serverError: (req, res)-> - res.status(500) - res.render 'general/500', - title: "Server Error" - - accountMergeError: (req, res)-> - res.status(500) - res.render 'general/account-merge-error', - title: "Account Access Error" - - handleError: (error, req, res, next) -> - user = AuthenticationController.getSessionUser(req) - if error?.code is 'EBADCSRFTOKEN' - logger.warn err: error,url:req.url, method:req.method, user:user, "invalid csrf" - res.sendStatus(403) - return - if error instanceof Errors.NotFoundError - logger.warn {err: error, url: req.url}, "not found error" - ErrorController.notFound req, res - else if error instanceof Errors.ForbiddenError - logger.error err: error, "forbidden error" - ErrorController.forbidden req, res - else if error instanceof Errors.TooManyRequestsError - logger.warn {err: error, url: req.url}, "too many requests error" - res.sendStatus(429) - else if error instanceof Errors.InvalidError - logger.warn {err: error, url: req.url}, "invalid error" - res.status(400) - res.send(error.message) - else if error instanceof Errors.InvalidNameError - logger.warn {err: error, url: req.url}, "invalid name error" - res.status(400) - res.send(error.message) - else if error instanceof Errors.AccountMergeError - logger.error err: error, "account merge error" - ErrorController.accountMergeError req, res - else - logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middleware" - ErrorController.serverError req, res - - handleApiError: (error, req, res, next) -> - if error instanceof Errors.NotFoundError - logger.warn {err: error, url: req.url}, "not found error" - res.sendStatus(404) - else - logger.error err: error, url:req.url, method:req.method, "error passed to top level next middleware" - res.sendStatus(500) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee deleted file mode 100644 index fb1d305f7d..0000000000 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ /dev/null @@ -1,147 +0,0 @@ -NotFoundError = (message) -> - error = new Error(message) - error.name = "NotFoundError" - error.__proto__ = NotFoundError.prototype - return error -NotFoundError.prototype.__proto__ = Error.prototype - -ForbiddenError = (message) -> - error = new Error(message) - error.name = "ForbiddenError" - error.__proto__ = ForbiddenError.prototype - return error -ForbiddenError.prototype.__proto__ = Error.prototype - -ServiceNotConfiguredError = (message) -> - error = new Error(message) - error.name = "ServiceNotConfiguredError" - error.__proto__ = ServiceNotConfiguredError.prototype - return error -ServiceNotConfiguredError.prototype.__proto__ = Error.prototype - -TooManyRequestsError = (message) -> - error = new Error(message) - error.name = "TooManyRequestsError" - error.__proto__ = TooManyRequestsError.prototype - return error -TooManyRequestsError.prototype.__proto__ = Error.prototype - -InvalidNameError = (message) -> - error = new Error(message) - error.name = "InvalidNameError" - error.__proto__ = InvalidNameError.prototype - return error -InvalidNameError.prototype.__proto__ = Error.prototype - -UnsupportedFileTypeError = (message) -> - error = new Error(message) - error.name = "UnsupportedFileTypeError" - error.__proto__ = UnsupportedFileTypeError.prototype - return error -UnsupportedFileTypeError.prototype.__proto__ = Error.prototype - -UnsupportedExportRecordsError = (message) -> - error = new Error(message) - error.name = "UnsupportedExportRecordsError" - error.__proto__ = UnsupportedExportRecordsError.prototype - return error -UnsupportedExportRecordsError.prototype.__proto__ = Error.prototype - -V1HistoryNotSyncedError = (message) -> - error = new Error(message) - error.name = "V1HistoryNotSyncedError" - error.__proto__ = V1HistoryNotSyncedError.prototype - return error -V1HistoryNotSyncedError.prototype.__proto__ = Error.prototype - -ProjectHistoryDisabledError = (message) -> - error = new Error(message) - error.name = "ProjectHistoryDisabledError" - error.__proto__ = ProjectHistoryDisabledError.prototype - return error -ProjectHistoryDisabledError.prototype.__proto__ = Error.prototype - -V1ConnectionError = (message) -> - error = new Error(message) - error.name = "V1ConnectionError" - error.__proto__ = V1ConnectionError.prototype - return error -V1ConnectionError.prototype.__proto__ = Error.prototype - -UnconfirmedEmailError = (message) -> - error = new Error(message) - error.name = "UnconfirmedEmailError" - error.__proto__ = UnconfirmedEmailError.prototype - return error -UnconfirmedEmailError.prototype.__proto__ = Error.prototype - -EmailExistsError = (message) -> - error = new Error(message) - error.name = "EmailExistsError" - error.__proto__ = EmailExistsError.prototype - return error -EmailExistsError.prototype.__proto__ = Error.prototype - -InvalidError = (message) -> - error = new Error(message) - error.name = "InvalidError" - error.__proto__ = InvalidError.prototype - return error -InvalidError.prototype.__proto__ = Error.prototype - -AccountMergeError = (message) -> - error = new Error(message) - error.name = "AccountMergeError" - error.__proto__ = AccountMergeError.prototype - return error -AccountMergeError.prototype.__proto__ = Error.prototype - -NotInV2Error = (message) -> - error = new Error(message) - error.name = "NotInV2Error" - error.__proto__ = NotInV2Error.prototype - return error -NotInV2Error.prototype.__proto__ = Error.prototype - -SLInV2Error = (message) -> - error = new Error(message) - error.name = "SLInV2Error" - error.__proto__ = SLInV2Error.prototype - return error -SLInV2Error.prototype.__proto__ = Error.prototype - -ThirdPartyUserNotFoundError = (message) -> - message = "user not found for provider and external id" unless message? - error = new Error(message) - error.name = "ThirdPartyUserNotFoundError" - error.__proto__ = SLInV2Error.prototype - return error -ThirdPartyUserNotFoundError.prototype.__proto__ = Error.prototype - -SubscriptionAdminDeletionError = (message) -> - message = "subscription admins cannot be deleted" unless message? - error = new Error(message) - error.name = "SubscriptionAdminDeletionError" - error.__proto__ = SubscriptionAdminDeletionError.prototype - return error -SubscriptionAdminDeletionError.prototype.__proto__ = Error.prototype - -module.exports = Errors = - NotFoundError: NotFoundError - ForbiddenError: ForbiddenError - ServiceNotConfiguredError: ServiceNotConfiguredError - TooManyRequestsError: TooManyRequestsError - InvalidNameError: InvalidNameError - UnsupportedFileTypeError: UnsupportedFileTypeError - UnsupportedExportRecordsError: UnsupportedExportRecordsError - V1HistoryNotSyncedError: V1HistoryNotSyncedError - ProjectHistoryDisabledError: ProjectHistoryDisabledError - V1ConnectionError: V1ConnectionError - UnconfirmedEmailError: UnconfirmedEmailError - EmailExistsError: EmailExistsError - InvalidError: InvalidError - AccountMergeError: AccountMergeError - NotInV2Error: NotInV2Error - SLInV2Error: SLInV2Error - ThirdPartyUserNotFoundError: ThirdPartyUserNotFoundError - SubscriptionAdminDeletionError: SubscriptionAdminDeletionError diff --git a/services/web/app/coffee/Features/Exports/ExportsController.coffee b/services/web/app/coffee/Features/Exports/ExportsController.coffee deleted file mode 100644 index 5ef5303e6a..0000000000 --- a/services/web/app/coffee/Features/Exports/ExportsController.coffee +++ /dev/null @@ -1,72 +0,0 @@ -ExportsHandler = require("./ExportsHandler") -AuthenticationController = require("../Authentication/AuthenticationController") -logger = require("logger-sharelatex") - -module.exports = - - exportProject: (req, res, next) -> - {project_id, brand_variation_id} = req.params - user_id = AuthenticationController.getLoggedInUserId(req) - export_params = { - project_id: project_id, - brand_variation_id: brand_variation_id, - user_id: user_id - } - - if req.body - export_params.first_name = req.body.firstName.trim() if req.body.firstName - export_params.last_name = req.body.lastName.trim() if req.body.lastName - # additional parameters for gallery exports - export_params.title = req.body.title.trim() if req.body.title - export_params.description = req.body.description.trim() if req.body.description - export_params.author = req.body.author.trim() if req.body.author - export_params.license = req.body.license.trim() if req.body.license - export_params.show_source = req.body.showSource if req.body.showSource? - - ExportsHandler.exportProject export_params, (err, export_data) -> - if err? - if err.forwardResponse? - logger.log {responseError: err.forwardResponse}, "forwarding response" - statusCode = err.forwardResponse.status || 500 - return res.status(statusCode).json err.forwardResponse - else - return next(err) - logger.log - user_id:user_id - project_id: project_id - brand_variation_id:brand_variation_id - export_v1_id:export_data.v1_id - "exported project" - res.json export_v1_id: export_data.v1_id - - exportStatus: (req, res) -> - {export_id} = req.params - ExportsHandler.fetchExport export_id, (err, export_json) -> - if err? - json = { - status_summary: 'failed', - status_detail: err.toString, - } - res.json export_json: json - return err - parsed_export = JSON.parse(export_json) - json = { - status_summary: parsed_export.status_summary, - status_detail: parsed_export.status_detail, - partner_submission_id: parsed_export.partner_submission_id, - v2_user_email: parsed_export.v2_user_email, - v2_user_first_name: parsed_export.v2_user_first_name, - v2_user_last_name: parsed_export.v2_user_last_name, - title: parsed_export.title, - token: parsed_export.token - } - res.json export_json: json - - exportDownload: (req, res, next) -> - {type, export_id} = req.params - - AuthenticationController.getLoggedInUserId(req) - ExportsHandler.fetchDownload export_id, type, (err, export_file_url) -> - return next(err) if err? - - res.redirect export_file_url diff --git a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee deleted file mode 100644 index 69aaf122a1..0000000000 --- a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee +++ /dev/null @@ -1,145 +0,0 @@ -ProjectGetter = require('../Project/ProjectGetter') -ProjectHistoryHandler = require('../Project/ProjectHistoryHandler') -ProjectLocator = require('../Project/ProjectLocator') -ProjectRootDocManager = require('../Project/ProjectRootDocManager') -UserGetter = require('../User/UserGetter') -logger = require('logger-sharelatex') -settings = require 'settings-sharelatex' -async = require 'async' -request = require 'request' -request = request.defaults() -settings = require 'settings-sharelatex' - -module.exports = ExportsHandler = self = - - exportProject: (export_params, callback=(error, export_data) ->) -> - self._buildExport export_params, (err, export_data) -> - return callback(err) if err? - self._requestExport export_data, (err, export_v1_id) -> - return callback(err) if err? - export_data.v1_id = export_v1_id - # TODO: possibly store the export data in Mongo - callback null, export_data - - _buildExport: (export_params, callback=(err, export_data) ->) -> - {project_id, user_id, brand_variation_id, title, description, author, - license, show_source} = export_params - jobs = - project: (cb) -> - ProjectGetter.getProject project_id, cb - # TODO: when we update async, signature will change from (cb, results) to (results, cb) - rootDoc: [ 'project', (cb, results) -> - ProjectRootDocManager.ensureRootDocumentIsValid project_id, (error) -> - return callback(error) if error? - ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb - ] - user: (cb) -> - UserGetter.getUser user_id, {first_name: 1, last_name: 1, email: 1, overleaf: 1}, cb - historyVersion: (cb) -> - ProjectHistoryHandler.ensureHistoryExistsForProject project_id, (error) -> - return callback(error) if error? - self._requestVersion project_id, cb - - async.auto jobs, (err, results) -> - if err? - logger.err err:err, project_id:project_id, user_id:user_id, brand_variation_id:brand_variation_id, "error building project export" - return callback(err) - - {project, rootDoc, user, historyVersion} = results - if !rootDoc[1]? - err = new Error("cannot export project without root doc") - logger.err err:err, project_id: project_id - return callback(err) - - if export_params.first_name && export_params.last_name - user.first_name = export_params.first_name - user.last_name = export_params.last_name - - export_data = - project: - id: project_id - rootDocPath: rootDoc[1]?.fileSystem - historyId: project.overleaf?.history?.id - historyVersion: historyVersion - v1ProjectId: project.overleaf?.id - metadata: - compiler: project.compiler - imageName: project.imageName - title: title - description: description - author: author - license: license - showSource: show_source - user: - id: user_id - firstName: user.first_name - lastName: user.last_name - email: user.email - orcidId: null # until v2 gets ORCID - v1UserId: user.overleaf?.id - destination: - brandVariationId: brand_variation_id - options: - callbackUrl: null # for now, until we want v1 to call us back - callback null, export_data - - _requestExport: (export_data, callback=(err, export_v1_id) ->) -> - request.post { - url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports" - auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass } - json: export_data - }, (err, res, body) -> - if err? - logger.err err:err, export:export_data, "error making request to v1 export" - callback err - else if 200 <= res.statusCode < 300 - callback null, body.exportId - else - logger.err export:export_data, "v1 export returned failure; forwarding: #{body}" - # pass the v1 error along for the publish modal to handle - callback {forwardResponse: body} - - _requestVersion: (project_id, callback=(err, export_v1_id) ->) -> - request.get { - url: "#{settings.apis.project_history.url}/project/#{project_id}/version" - json: true - }, (err, res, body) -> - if err? - logger.err err:err, project_id:project_id, "error making request to project history" - callback err - else if res.statusCode >= 200 and res.statusCode < 300 - callback null, body.version - else - err = new Error("project history version returned a failure status code: #{res.statusCode}") - logger.err err:err, project_id:project_id, "project history version returned failure status code: #{res.statusCode}" - callback err - - fetchExport: (export_id, callback=(err, export_json) ->) -> - request.get { - url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}" - auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass } - }, (err, res, body) -> - if err? - logger.err err:err, export:export_id, "error making request to v1 export" - callback err - else if 200 <= res.statusCode < 300 - callback null, body - else - err = new Error("v1 export returned a failure status code: #{res.statusCode}") - logger.err err:err, export:export_id, "v1 export returned failure status code: #{res.statusCode}" - callback err - - fetchDownload: (export_id, type, callback=(err, file_url) ->) -> - request.get { - url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/#{type}_url" - auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass } - }, (err, res, body) -> - if err? - logger.err err:err, export:export_id, "error making request to v1 export" - callback err - else if 200 <= res.statusCode < 300 - callback null, body - else - err = new Error("v1 export returned a failure status code: #{res.statusCode}") - logger.err err:err, export:export_id, "v1 export zip fetch returned failure status code: #{res.statusCode}" - callback err diff --git a/services/web/app/coffee/Features/FileStore/FileHashManager.coffee b/services/web/app/coffee/Features/FileStore/FileHashManager.coffee deleted file mode 100644 index 8de0291512..0000000000 --- a/services/web/app/coffee/Features/FileStore/FileHashManager.coffee +++ /dev/null @@ -1,35 +0,0 @@ -crypto = require "crypto" -logger = require("logger-sharelatex") -fs = require("fs") -_ = require("underscore") - -module.exports = FileHashManager = - - computeHash: (filePath, callback = (error, hashValue) ->) -> - callback = _.once(callback) # avoid double callbacks - - # taken from v1/history/storage/lib/blob_hash.js - getGitBlobHeader = (byteLength) -> - return 'blob ' + byteLength + '\x00' - - getByteLengthOfFile = (cb) -> - fs.stat filePath, (err, stats) -> - return cb(err) if err? - cb(null, stats.size) - - getByteLengthOfFile (err, byteLength) -> - return callback(err) if err? - - input = fs.createReadStream(filePath) - input.on 'error', (err) -> - logger.err {filePath: filePath, err:err}, "error opening file in computeHash" - return callback(err) - - hash = crypto.createHash("sha1") - hash.setEncoding('hex') - hash.update(getGitBlobHeader(byteLength)) - hash.on 'readable', () -> - result = hash.read() - if result? - callback(null, result.toString('hex')) - input.pipe(hash) \ No newline at end of file diff --git a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee deleted file mode 100644 index eceb5aee4f..0000000000 --- a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee +++ /dev/null @@ -1,39 +0,0 @@ -logger = require('logger-sharelatex') -FileStoreHandler = require("./FileStoreHandler") -ProjectLocator = require("../Project/ProjectLocator") -_ = require('underscore') - -is_mobile_safari = (user_agent) -> - user_agent and (user_agent.indexOf('iPhone') >= 0 or - user_agent.indexOf('iPad') >= 0) - -is_html = (file) -> - ends_with = (ext) -> - file.name? and - file.name.length > ext.length and - (file.name.lastIndexOf(ext) == file.name.length - ext.length) - - ends_with('.html') or ends_with('.htm') or ends_with('.xhtml') - -module.exports = - - getFile : (req, res)-> - project_id = req.params.Project_id - file_id = req.params.File_id - queryString = req.query - user_agent = req.get('User-Agent') - logger.log project_id: project_id, file_id: file_id, queryString:queryString, "file download" - ProjectLocator.findElement {project_id: project_id, element_id: file_id, type: "file"}, (err, file)-> - if err? - logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error finding element for downloading file" - return res.sendStatus 500 - FileStoreHandler.getFileStream project_id, file_id, queryString, (err, stream)-> - if err? - logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error getting file stream for downloading file" - return res.sendStatus 500 - # mobile safari will try to render html files, prevent this - if (is_mobile_safari(user_agent) and is_html(file)) - logger.log filename: file.name, user_agent: user_agent, "sending html file to mobile-safari as plain text" - res.setHeader('Content-Type', 'text/plain') - res.setContentDisposition('attachment', {filename: file.name}) - stream.pipe res diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee deleted file mode 100644 index 4e080902a2..0000000000 --- a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee +++ /dev/null @@ -1,124 +0,0 @@ -logger = require("logger-sharelatex") -fs = require("fs") -request = require("request") -settings = require("settings-sharelatex") -Async = require('async') -FileHashManager = require("./FileHashManager") -File = require('../../models/File').File - -oneMinInMs = 60 * 1000 -fiveMinsInMs = oneMinInMs * 5 - -module.exports = FileStoreHandler = - - RETRY_ATTEMPTS: 3 - - uploadFileFromDisk: (project_id, file_args, fsPath, callback = (error, url, fileRef) ->)-> - fs.lstat fsPath, (err, stat)-> - if err? - logger.err err:err, project_id:project_id, file_args:file_args, fsPath:fsPath, "error stating file" - callback(err) - if !stat? - logger.err project_id:project_id, file_args:file_args, fsPath:fsPath, "stat is not available, can not check file from disk" - return callback(new Error("error getting stat, not available")) - if !stat.isFile() - logger.log project_id:project_id, file_args:file_args, fsPath:fsPath, "tried to upload symlink, not contining" - return callback(new Error("can not upload symlink")) - Async.retry FileStoreHandler.RETRY_ATTEMPTS, (cb, results) -> - FileStoreHandler._doUploadFileFromDisk project_id, file_args, fsPath, cb - , (err, result) -> - if err? - logger.err {err, project_id, file_args}, "Error uploading file, retries failed" - return callback(err) - callback(err, result.url, result.fileRef) - - _doUploadFileFromDisk: (project_id, file_args, fsPath, callback = (err, result)->) -> - _cb = callback - callback = (err, result...) -> - callback = -> # avoid double callbacks - _cb(err, result...) - - FileHashManager.computeHash fsPath, (err, hashValue) -> - return callback(err) if err? - fileRef = new File(Object.assign({}, file_args, {hash: hashValue})) - file_id = fileRef._id - logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, hash: hashValue, fileRef:fileRef, "uploading file from disk" - readStream = fs.createReadStream(fsPath) - readStream.on "error", (err)-> - logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk" - callback err - readStream.on "open", () -> - url = FileStoreHandler._buildUrl(project_id, file_id) - opts = - method: "post" - uri: url - timeout:fiveMinsInMs - headers: - "X-File-Hash-From-Web": hashValue # send the hash to the filestore as a custom header so it can be checked - writeStream = request(opts) - writeStream.on "error", (err)-> - logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk" - callback err - writeStream.on 'response', (response) -> - if response.statusCode not in [200, 201] - err = new Error("non-ok response from filestore for upload: #{response.statusCode}") - logger.err {err, statusCode: response.statusCode}, "error uploading to filestore" - callback(err) - else - callback(null, {url, fileRef}) # have to pass back an object because async.retry only accepts a single result argument - readStream.pipe writeStream - - getFileStream: (project_id, file_id, query, callback)-> - logger.log project_id:project_id, file_id:file_id, query:query, "getting file stream from file store" - queryString = "" - if query? and query["format"]? - queryString = "?format=#{query['format']}" - opts = - method : "get" - uri: "#{@_buildUrl(project_id, file_id)}#{queryString}" - timeout:fiveMinsInMs - headers: {} - if query? and query['range']? - rangeText = query['range'] - if rangeText && rangeText.match? && rangeText.match(/\d+-\d+/) - opts.headers['range'] = "bytes=#{query['range']}" - readStream = request(opts) - readStream.on "error", (err) -> - logger.err {err, project_id, file_id, query, opts}, "error in file stream" - callback(null, readStream) - - deleteFile: (project_id, file_id, callback)-> - logger.log project_id:project_id, file_id:file_id, "telling file store to delete file" - opts = - method : "delete" - uri: @_buildUrl(project_id, file_id) - timeout:fiveMinsInMs - request opts, (err, response)-> - if err? - logger.err err:err, project_id:project_id, file_id:file_id, "something went wrong deleting file from filestore" - callback(err) - - copyFile: (oldProject_id, oldFile_id, newProject_id, newFile_id, callback)-> - logger.log oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "telling filestore to copy a file" - opts = - method : "put" - json: - source: - project_id:oldProject_id - file_id:oldFile_id - uri: @_buildUrl(newProject_id, newFile_id) - timeout:fiveMinsInMs - request opts, (err, response)-> - if err? - logger.err err:err, oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "something went wrong telling filestore api to copy file" - callback(err) - else if 200 <= response.statusCode < 300 - # successful response - callback(null, opts.uri) - else - err = new Error("non-ok response from filestore for copyFile: #{response.statusCode}") - logger.err {uri: opts.uri, statusCode: response.statusCode}, "error uploading to filestore" - callback(err) - - _buildUrl: (project_id, file_id)-> - return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}" diff --git a/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee b/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee deleted file mode 100644 index 79163ded48..0000000000 --- a/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee +++ /dev/null @@ -1,87 +0,0 @@ -Mocha = require "mocha" -Base = require("mocha/lib/reporters/base") -RedisWrapper = require("../../infrastructure/RedisWrapper") -rclient = RedisWrapper.client("health_check") -settings = require("settings-sharelatex") -logger = require "logger-sharelatex" -domain = require "domain" -UserGetter = require("../User/UserGetter") - -module.exports = HealthCheckController = - check: (req, res, next = (error) ->) -> - d = domain.create() - d.on "error", (error) -> - logger.err err: error, "error in mocha" - d.run () -> - mocha = new Mocha(reporter: Reporter(res), timeout: 10000) - mocha.addFile("test/smoke/js/SmokeTests.js") - mocha.run () -> - # TODO: combine this with the smoke-test-sharelatex module - # we need to clean up all references to the smokeTest module - # so it can be garbage collected. The only reference should - # be in its parent, when it is loaded by mocha.addFile. - path = require.resolve(__dirname + "/../../../../test/smoke/js/SmokeTests.js") - smokeTestModule = require.cache[path] - if smokeTestModule? - parent = smokeTestModule.parent - while (idx = parent.children.indexOf(smokeTestModule)) != -1 - parent.children.splice(idx, 1) - else - logger.warn {path}, "smokeTestModule not defined" - # remove the smokeTest from the module cache - delete require.cache[path] - - checkRedis: (req, res, next)-> - rclient.healthCheck (error) -> - if error? - logger.err {err: error}, "failed redis health check" - res.sendStatus 500 - else - res.sendStatus 200 - - checkMongo: (req, res, next)-> - logger.log "running mongo health check" - UserGetter.getUserEmail settings.smokeTest.userId, (err, email)-> - if err? - logger.err err:err, "mongo health check failed, error present" - return res.sendStatus 500 - else if !email? - logger.err err:err, "mongo health check failed, no emai present in find result" - return res.sendStatus 500 - else - logger.log email:email, "mongo health check passed" - res.sendStatus 200 - - -Reporter = (res) -> - (runner) -> - Base.call(this, runner) - - tests = [] - passes = [] - failures = [] - - runner.on 'test end', (test) -> tests.push(test) - runner.on 'pass', (test) -> passes.push(test) - runner.on 'fail', (test) -> failures.push(test) - - runner.on 'end', () => - clean = (test) -> - title: test.fullTitle() - duration: test.duration - err: test.err - timedOut: test.timedOut - - results = { - stats: @stats - failures: failures.map(clean) - passes: passes.map(clean) - } - - res.contentType("application/json") - if failures.length > 0 - logger.err failures:failures, "health check failed" - res.status(500).send(JSON.stringify(results, null, 2)) - else - res.status(200).send(JSON.stringify(results, null, 2)) - diff --git a/services/web/app/coffee/Features/Helpers/EmailHelper.coffee b/services/web/app/coffee/Features/Helpers/EmailHelper.coffee deleted file mode 100644 index 8b769c1669..0000000000 --- a/services/web/app/coffee/Features/Helpers/EmailHelper.coffee +++ /dev/null @@ -1,13 +0,0 @@ -EMAIL_REGEXP = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - -module.exports = EmailHelper = - - parseEmail: (email) -> - return null unless email? - return null if email.length > 254 - email = email.trim().toLowerCase() - - matched = email.match EMAIL_REGEXP - return null unless matched? && matched[0]? - - matched[0] diff --git a/services/web/app/coffee/Features/Helpers/StringHelper.coffee b/services/web/app/coffee/Features/Helpers/StringHelper.coffee deleted file mode 100644 index f13c5afa3a..0000000000 --- a/services/web/app/coffee/Features/Helpers/StringHelper.coffee +++ /dev/null @@ -1,18 +0,0 @@ -JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/g - -JSON_ESCAPE = - '&': '\\u0026' - '>': '\\u003e' - '<': '\\u003c' - '\u2028': '\\u2028' - '\u2029': '\\u2029' - -module.exports = StringHelper = - # stringifies and escapes a json object for use in a script. This ensures that &, < and > characters are escaped, - # along with quotes. This ensures that the string can be safely rendered into HTML. See rationale at: - # https://api.rubyonrails.org/classes/ERB/Util.html#method-c-json_escape - # and implementation lifted from: - # https://github.com/ember-fastboot/fastboot/blob/cafd96c48564d8384eb83dc908303dba8ece10fd/src/ember-app.js#L496-L510 - stringifyJsonForScript: (object) -> - return JSON.stringify(object).replace JSON_ESCAPE_REGEXP, (match) -> - return JSON_ESCAPE[match] diff --git a/services/web/app/coffee/Features/Helpers/UrlHelper.coffee b/services/web/app/coffee/Features/Helpers/UrlHelper.coffee deleted file mode 100644 index 6e28f1ed73..0000000000 --- a/services/web/app/coffee/Features/Helpers/UrlHelper.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Settings = require 'settings-sharelatex' - -module.exports = UrlHelper = - - wrapUrlWithProxy: (url) -> - # TODO: Consider what to do for Community and Enterprise edition? - if !Settings.apis?.linkedUrlProxy?.url? - throw new Error('no linked url proxy configured') - return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}" - - prependHttpIfNeeded: (url) -> - if !url.match('://') - url = 'http://' + url - return url \ No newline at end of file diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee deleted file mode 100644 index c11dcd8deb..0000000000 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ /dev/null @@ -1,209 +0,0 @@ -_ = require "lodash" -async = require "async" -logger = require "logger-sharelatex" -request = require "request" -settings = require "settings-sharelatex" -AuthenticationController = require "../Authentication/AuthenticationController" -Errors = require "../Errors/Errors" -HistoryManager = require "./HistoryManager" -ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" -ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" -RestoreManager = require "./RestoreManager" - -module.exports = HistoryController = - selectHistoryApi: (req, res, next = (error) ->) -> - project_id = req.params?.Project_id - # find out which type of history service this project uses - ProjectDetailsHandler.getDetails project_id, (err, project) -> - return next(err) if err? - history = project.overleaf?.history - if history?.id? and history?.display - req.useProjectHistory = true - else - req.useProjectHistory = false - next() - - ensureProjectHistoryEnabled: (req, res, next = (error) ->) -> - if req.useProjectHistory? - next() - else - logger.log {project_id}, "project history not enabled" - res.sendStatus(404) - - proxyToHistoryApi: (req, res, next = (error) ->) -> - user_id = AuthenticationController.getLoggedInUserId req - url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url - - logger.log url: url, "proxying to history api" - getReq = request( - url: url - method: req.method - headers: - "X-User-Id": user_id - ) - getReq.pipe(res) - getReq.on "error", (error) -> - logger.error url: url, err: error, "history API error" - next(error) - - proxyToHistoryApiAndInjectUserDetails: (req, res, next = (error) ->) -> - user_id = AuthenticationController.getLoggedInUserId req - url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url - logger.log url: url, "proxying to history api" - HistoryController._makeRequest { - url: url - method: req.method - json: true - headers: - "X-User-Id": user_id - }, (error, body) -> - return next(error) if error? - HistoryManager.injectUserDetails body, (error, data) -> - return next(error) if error? - res.json data - - buildHistoryServiceUrl: (useProjectHistory) -> - # choose a history service, either document-level (trackchanges) - # or project-level (project_history) - if useProjectHistory - return settings.apis.project_history.url - else - return settings.apis.trackchanges.url - - resyncProjectHistory: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - ProjectEntityUpdateHandler.resyncProjectHistory project_id, (error) -> - return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError - return next(error) if error? - res.sendStatus 204 - - restoreFileFromV2: (req, res, next) -> - {project_id} = req.params - {version, pathname} = req.body - user_id = AuthenticationController.getLoggedInUserId req - logger.log {project_id, version, pathname}, "restoring file from v2" - RestoreManager.restoreFileFromV2 user_id, project_id, version, pathname, (error, entity) -> - return next(error) if error? - res.json { - type: entity.type, - id: entity._id - } - - restoreDocFromDeletedDoc: (req, res, next) -> - {project_id, doc_id} = req.params - {name} = req.body - user_id = AuthenticationController.getLoggedInUserId(req) - if !name? - return res.sendStatus 400 # Malformed request - logger.log {project_id, doc_id, user_id}, "restoring doc from v1 deleted doc" - RestoreManager.restoreDocFromDeletedDoc user_id, project_id, doc_id, name, (err, doc) => - return next(error) if error? - res.json { - doc_id: doc._id - } - - getLabels: (req, res, next) -> - project_id = req.params.Project_id - user_id = AuthenticationController.getLoggedInUserId(req) - HistoryController._makeRequest { - method: "GET" - url: "#{settings.apis.project_history.url}/project/#{project_id}/labels" - json: true - }, (error, labels) -> - return next(error) if error? - res.json labels - - createLabel: (req, res, next) -> - project_id = req.params.Project_id - {comment, version} = req.body - user_id = AuthenticationController.getLoggedInUserId(req) - HistoryController._makeRequest { - method: "POST" - url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels" - json: {comment, version} - }, (error, label) -> - return next(error) if error? - res.json label - - deleteLabel: (req, res, next) -> - project_id = req.params.Project_id - label_id = req.params.label_id - user_id = AuthenticationController.getLoggedInUserId(req) - HistoryController._makeRequest { - method: "DELETE" - url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}" - }, (error) -> - return next(error) if error? - res.sendStatus 204 - - _makeRequest: (options, callback) -> - request options, (error, response, body) -> - return callback(error) if error? - if 200 <= response.statusCode < 300 - callback(null, body) - else - error = new Error("history api responded with non-success code: #{response.statusCode}") - logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}" - callback(error) - - downloadZipOfVersion: (req, res, next) -> - {project_id, version} = req.params - logger.log {project_id, version}, "got request for zip file at version" - ProjectDetailsHandler.getDetails project_id, (err, project) -> - return next(err) if err? - v1_id = project.overleaf?.history?.id - if !v1_id? - logger.err {project_id, version}, 'got request for zip version of non-v1 history project' - return res.sendStatus(402) - HistoryController._pipeHistoryZipToResponse v1_id, version, "#{project.name} (Version #{version})", res, next - - _pipeHistoryZipToResponse: (v1_project_id, version, name, res, next) -> - # increase timeout to 6 minutes - res.setTimeout(6 * 60 * 1000) - url = "#{settings.apis.v1_history.url}/projects/#{v1_project_id}/version/#{version}/zip" - logger.log {v1_project_id, version, url}, "getting s3 url from history api" - options = - auth: - user: settings.apis.v1_history.user - pass: settings.apis.v1_history.pass - json: true - method: 'post' - url: url - request options, (err, response, body) -> - if err - logger.error {err, v1_project_id, version}, "history API error" - return next(err) - retryAttempt = 0 - retryDelay = 2000 - # retry for about 6 minutes starting with short delay - async.retry 40, - (callback) -> - setTimeout(() -> - # increase delay by 1 second up to 10 - retryDelay += 1000 if retryDelay < 10000 - retryAttempt++ - getReq = request( - url: body.zipUrl - sendImmediately: true - ) - getReq.on 'response', (response) -> - return callback new Error "invalid response" unless response.statusCode == 200 - # pipe also proxies the headers, but we want to customize these ones - delete response.headers['content-disposition'] - delete response.headers['content-type'] - res.status response.statusCode - res.setContentDisposition( - 'attachment', - {filename: "#{name}.zip"} - ) - res.contentType('application/zip') - getReq.pipe(res) - callback() - getReq.on "error", (err) -> - logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download error" - callback(err) - , retryDelay) - (err) -> - if err - logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download failed" - next(err) diff --git a/services/web/app/coffee/Features/History/HistoryManager.coffee b/services/web/app/coffee/Features/History/HistoryManager.coffee deleted file mode 100644 index 0a45029b9b..0000000000 --- a/services/web/app/coffee/Features/History/HistoryManager.coffee +++ /dev/null @@ -1,99 +0,0 @@ -request = require "request" -settings = require "settings-sharelatex" -async = require 'async' -UserGetter = require "../User/UserGetter" - -module.exports = HistoryManager = - initializeProject: (callback = (error, history_id) ->) -> - return callback() if !settings.apis.project_history?.initializeHistoryForNewProjects - request.post { - url: "#{settings.apis.project_history.url}/project" - }, (error, res, body)-> - return callback(error) if error? - - if res.statusCode >= 200 and res.statusCode < 300 - try - project = JSON.parse(body) - catch error - return callback(error) - - overleaf_id = project?.project?.id - if !overleaf_id - error = new Error("project-history did not provide an id", project) - return callback(error) - - callback null, { overleaf_id } - else - error = new Error("project-history returned a non-success status code: #{res.statusCode}") - callback error - - flushProject: (project_id, callback = (error) ->) -> - request.post { - url: "#{settings.apis.project_history.url}/project/#{project_id}/flush" - }, (error, res, body)-> - return callback(error) if error? - if res.statusCode >= 200 and res.statusCode < 300 - callback() - else - error = new Error("project-history returned a non-success status code: #{res.statusCode}") - callback error - - resyncProject: (project_id, callback = (error) ->) -> - request.post { - url: "#{settings.apis.project_history.url}/project/#{project_id}/resync" - }, (error, res, body)-> - return callback(error) if error? - if res.statusCode >= 200 and res.statusCode < 300 - callback() - else - error = new Error("project-history returned a non-success status code: #{res.statusCode}") - callback error - - injectUserDetails: (data, callback = (error, data_with_users) ->) -> - # data can be either: - # { - # diff: [{ - # i: "foo", - # meta: { - # users: ["user_id", { first_name: "James", ... }, ...] - # ... - # } - # }, ...] - # } - # or - # { - # updates: [{ - # pathnames: ["main.tex"] - # meta: { - # users: ["user_id", { first_name: "James", ... }, ...] - # ... - # }, - # ... - # }, ...] - # } - # Either way, the top level key points to an array of objects with a meta.users property - # that we need to replace user_ids with populated user objects. - # Note that some entries in the users arrays may already have user objects from the v1 history - # service - user_ids = new Set() - for entry in data.diff or data.updates or [] - for user in entry.meta?.users or [] - if typeof user == "string" - user_ids.add user - user_ids = Array.from(user_ids) - UserGetter.getUsers user_ids, { first_name: 1, last_name: 1, email: 1 }, (error, users_array) -> - return callback(error) if error? - users = {} - for user in users_array or [] - users[user._id.toString()] = HistoryManager._userView(user) - for entry in data.diff or data.updates or [] - entry.meta?.users = (entry.meta?.users or []).map (user) -> - if typeof user == "string" - return users[user] - else - return user - callback null, data - - _userView: (user) -> - { _id, first_name, last_name, email } = user - return { first_name, last_name, email, id: _id } \ No newline at end of file diff --git a/services/web/app/coffee/Features/History/RestoreManager.coffee b/services/web/app/coffee/Features/History/RestoreManager.coffee deleted file mode 100644 index ba7af44c5e..0000000000 --- a/services/web/app/coffee/Features/History/RestoreManager.coffee +++ /dev/null @@ -1,60 +0,0 @@ -Settings = require 'settings-sharelatex' -Path = require 'path' -FileWriter = require '../../infrastructure/FileWriter' -FileSystemImportManager = require '../Uploads/FileSystemImportManager' -ProjectEntityHandler = require '../Project/ProjectEntityHandler' -ProjectLocator = require '../Project/ProjectLocator' -EditorController = require '../Editor/EditorController' -Errors = require '../Errors/Errors' -moment = require 'moment' - -module.exports = RestoreManager = - restoreDocFromDeletedDoc: (user_id, project_id, doc_id, name, callback = (error, doc, folder_id) ->) -> - # This is the legacy method for restoring a doc from the SL track-changes/deletedDocs system. - # It looks up the deleted doc's contents, and then creates a new doc with the same content. - # We don't actually remove the deleted doc entry, just create a new one from its lines. - ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) -> - return callback(error) if error? - addDocWithName = (name, callback) -> - EditorController.addDoc project_id, null, name, lines, 'restore', user_id, callback - RestoreManager._addEntityWithUniqueName addDocWithName, name, callback - - restoreFileFromV2: (user_id, project_id, version, pathname, callback = (error, entity) ->) -> - RestoreManager._writeFileVersionToDisk project_id, version, pathname, (error, fsPath) -> - return callback(error) if error? - basename = Path.basename(pathname) - dirname = Path.dirname(pathname) - if dirname == '.' # no directory - dirname = '' - RestoreManager._findOrCreateFolder project_id, dirname, (error, parent_folder_id) -> - return callback(error) if error? - addEntityWithName = (name, callback) -> - FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, name, fsPath, false, callback - RestoreManager._addEntityWithUniqueName addEntityWithName, basename, callback - - _findOrCreateFolder: (project_id, dirname, callback = (error, folder_id) ->) -> - EditorController.mkdirp project_id, dirname, (error, newFolders, lastFolder) -> - return callback(error) if error? - return callback(null, lastFolder?._id) - - _addEntityWithUniqueName: (addEntityWithName, basename, callback = (error) ->) -> - addEntityWithName basename, (error, entity) -> - if error? - if error instanceof Errors.InvalidNameError - # likely a duplicate name, so try with a prefix - date = moment(new Date()).format('Do MMM YY H:mm:ss') - # Move extension to the end so the file type is preserved - extension = Path.extname(basename) - basename = Path.basename(basename, extension) - basename = "#{basename} (Restored on #{date})" - if extension != '' - basename = "#{basename}#{extension}" - addEntityWithName basename, callback - else - callback(error) - else - callback(null, entity) - - _writeFileVersionToDisk: (project_id, version, pathname, callback = (error, fsPath) ->) -> - url = "#{Settings.apis.project_history.url}/project/#{project_id}/version/#{version}/#{encodeURIComponent(pathname)}" - FileWriter.writeUrlToDisk project_id, url, callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee deleted file mode 100644 index 03db848838..0000000000 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee +++ /dev/null @@ -1,25 +0,0 @@ -InactiveProjectManager = require("./InactiveProjectManager") -logger = require("logger-sharelatex") - - -module.exports = - - deactivateOldProjects: (req, res)-> - logger.log "recived request to deactivate old projects" - numberOfProjectsToArchive = parseInt(req.body.numberOfProjectsToArchive, 10) - ageOfProjects = req.body.ageOfProjects - InactiveProjectManager.deactivateOldProjects numberOfProjectsToArchive, ageOfProjects, (err, projectsDeactivated)-> - if err? - res.sendStatus(500) - else - res.send(projectsDeactivated) - - - deactivateProject: (req, res)-> - project_id = req.params.project_id - logger.log project_id:project_id, "recived request to deactivating project" - InactiveProjectManager.deactivateProject project_id, (err)-> - if err? - res.sendStatus 500 - else - res.sendStatus 200 \ No newline at end of file diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee deleted file mode 100644 index 5c984dcb5d..0000000000 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee +++ /dev/null @@ -1,59 +0,0 @@ -async = require("async") -_ = require("underscore") -logger = require("logger-sharelatex") -DocstoreManager = require("../Docstore/DocstoreManager") -ProjectGetter = require("../Project/ProjectGetter") -ProjectUpdateHandler = require("../Project/ProjectUpdateHandler") -Project = require("../../models/Project").Project - -MILISECONDS_IN_DAY = 86400000 -module.exports = InactiveProjectManager = - - reactivateProjectIfRequired: (project_id, callback)-> - ProjectGetter.getProject project_id, {active:true}, (err, project)-> - if err? - logger.err err:err, project_id:project_id, "error getting project" - return callback(err) - logger.log project_id:project_id, active:project.active, "seeing if need to reactivate project" - - if project.active - return callback() - - DocstoreManager.unarchiveProject project_id, (err)-> - if err? - logger.err err:err, project_id:project_id, "error reactivating project in docstore" - return callback(err) - ProjectUpdateHandler.markAsActive project_id, callback - - deactivateOldProjects: (limit = 10, daysOld = 360, callback)-> - oldProjectDate = new Date() - (MILISECONDS_IN_DAY * daysOld) - logger.log oldProjectDate:oldProjectDate, limit:limit, daysOld:daysOld, "starting process of deactivating old projects" - Project.find() - .where("lastOpened").lt(oldProjectDate) - .where("active").equals(true) - .select("_id") - .limit(limit) - .exec (err, projects)-> - if err? - logger.err err:err, "could not get projects for deactivating" - jobs = _.map projects, (project)-> - return (cb)-> - InactiveProjectManager.deactivateProject project._id, cb - logger.log numberOfProjects:projects?.length, "deactivating projects" - async.series jobs, (err)-> - if err? - logger.err err:err, "error deactivating projects" - callback err, projects - - - deactivateProject: (project_id, callback)-> - logger.log project_id:project_id, "deactivating inactive project" - jobs = [ - (cb)-> DocstoreManager.archiveProject project_id, cb - (cb)-> ProjectUpdateHandler.markAsInactive project_id, cb - ] - async.series jobs, (err)-> - if err? - logger.err err:err, project_id:project_id, "error deactivating project" - callback(err) - diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee deleted file mode 100644 index e996c5020f..0000000000 --- a/services/web/app/coffee/Features/Institutions/InstitutionsAPI.coffee +++ /dev/null @@ -1,111 +0,0 @@ -logger = require("logger-sharelatex") -metrics = require("metrics-sharelatex") -settings = require "settings-sharelatex" -request = require "request" -NotificationsBuilder = require("../Notifications/NotificationsBuilder") - -module.exports = InstitutionsAPI = - getInstitutionAffiliations: (institutionId, callback = (error, body) ->) -> - makeAffiliationRequest { - method: 'GET' - path: "/api/v2/institutions/#{institutionId.toString()}/affiliations" - defaultErrorMessage: "Couldn't get institution affiliations" - }, (error, body) -> callback(error, body or []) - - getInstitutionLicences: (institutionId, startDate, endDate, lag, callback = (error, body) ->) -> - makeAffiliationRequest { - method: 'GET' - path: "/api/v2/institutions/#{institutionId.toString()}/institution_licences" - body: {start_date: startDate, end_date: endDate, lag} - defaultErrorMessage: "Couldn't get institution licences" - }, callback - - getUserAffiliations: (userId, callback = (error, body) ->) -> - makeAffiliationRequest { - method: 'GET' - path: "/api/v2/users/#{userId.toString()}/affiliations" - defaultErrorMessage: "Couldn't get user affiliations" - }, (error, body) -> callback(error, body or []) - - addAffiliation: (userId, email, affiliationOptions, callback) -> - unless callback? # affiliationOptions is optional - callback = affiliationOptions - affiliationOptions = {} - - { university, department, role, confirmedAt } = affiliationOptions - makeAffiliationRequest { - method: 'POST' - path: "/api/v2/users/#{userId.toString()}/affiliations" - body: { email, university, department, role, confirmedAt } - defaultErrorMessage: "Couldn't create affiliation" - }, (error, body) -> - if error - return callback(error, body) - # have notifications delete any ip matcher notifications for this university - logger.log university - NotificationsBuilder.ipMatcherAffiliation(userId).read university?.id, (err) -> - if err - logger.err err:err, "Something went wrong marking ip notifications read" - callback(error, body) - - removeAffiliation: (userId, email, callback = (error) ->) -> - makeAffiliationRequest { - method: 'POST' - path: "/api/v2/users/#{userId.toString()}/affiliations/remove" - body: { email } - extraSuccessStatusCodes: [404] # `Not Found` responses are considered successful - defaultErrorMessage: "Couldn't remove affiliation" - }, callback - - - endorseAffiliation: (userId, email, role, department, callback = (error) ->) -> - makeAffiliationRequest { - method: 'POST' - path: "/api/v2/users/#{userId.toString()}/affiliations/endorse" - body: { email, role, department } - defaultErrorMessage: "Couldn't endorse affiliation" - }, callback - - - deleteAffiliations: (userId, callback = (error) ->) -> - makeAffiliationRequest { - method: 'DELETE' - path: "/api/v2/users/#{userId.toString()}/affiliations" - defaultErrorMessage: "Couldn't delete affiliations" - }, callback - - -makeAffiliationRequest = (requestOptions, callback = (error) ->) -> - return callback(null) unless settings?.apis?.v1?.url # service is not configured - requestOptions.extraSuccessStatusCodes ||= [] - request { - method: requestOptions.method - url: "#{settings.apis.v1.url}#{requestOptions.path}" - body: requestOptions.body - auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } - json: true, - timeout: 20 * 1000 - }, (error, response, body) -> - return callback(error) if error? - isSuccess = 200 <= response.statusCode < 300 - isSuccess ||= response.statusCode in requestOptions.extraSuccessStatusCodes - unless isSuccess - if body?.errors - errorMessage = "#{response.statusCode}: #{body.errors}" - else - errorMessage = "#{requestOptions.defaultErrorMessage}: #{response.statusCode}" - - logger.err path: requestOptions.path, body: requestOptions.body, errorMessage - return callback(new Error(errorMessage)) - - callback(null, body) - -[ - 'getInstitutionAffiliations' - 'getUserAffiliations', - 'addAffiliation', - 'removeAffiliation', -].map (method) -> - metrics.timeAsyncMethod( - InstitutionsAPI, method, 'mongo.InstitutionsAPI', logger - ) diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsController.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsController.coffee deleted file mode 100644 index f687c8a000..0000000000 --- a/services/web/app/coffee/Features/Institutions/InstitutionsController.coffee +++ /dev/null @@ -1,33 +0,0 @@ -logger = require("logger-sharelatex") -UserGetter = require("../User/UserGetter") -{ addAffiliation } = require("../Institutions/InstitutionsAPI") -FeaturesUpdater = require('../Subscription/FeaturesUpdater') -async = require('async') - -module.exports = InstitutionsController = - confirmDomain: (req, res, next) -> - hostname = req.body.hostname - affiliateUsers hostname, (error) -> - return next(error) if error? - res.sendStatus 200 - -affiliateUsers = (hostname, callback = (error)->) -> - reversedHostname = hostname.trim().split('').reverse().join('') - UserGetter.getUsersByHostname hostname, {_id:1, emails:1}, (error, users) -> - if error? - logger.err error: error, 'problem fetching users by hostname' - return callback(error) - - async.map users, ((user, innerCallback) -> - affiliateUserByReversedHostname user, reversedHostname, innerCallback - ), callback - -affiliateUserByReversedHostname = (user, reversedHostname, callback) -> - matchingEmails = user.emails.filter (email) -> email.reversedHostname == reversedHostname - async.map matchingEmails, ((email, innerCallback) -> - addAffiliation user._id, email.email, {confirmedAt: email.confirmedAt}, (error) => - if error? - logger.err error: error, 'problem adding affiliation while confirming hostname' - return innerCallback(error) - FeaturesUpdater.refreshFeatures user._id, true, innerCallback - ), callback diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee deleted file mode 100644 index d780461492..0000000000 --- a/services/web/app/coffee/Features/Institutions/InstitutionsFeatures.coffee +++ /dev/null @@ -1,28 +0,0 @@ -InstitutionsGetter = require './InstitutionsGetter' -PlansLocator = require '../Subscription/PlansLocator' -Settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' - -module.exports = InstitutionsFeatures = - getInstitutionsFeatures: (userId, callback = (error, features) ->) -> - InstitutionsFeatures.getInstitutionsPlan userId, (error, plan) -> - return callback error if error? - plan = PlansLocator.findLocalPlanInSettings plan - callback(null, plan?.features or {}) - - - getInstitutionsPlan: (userId, callback = (error, plan) ->) -> - InstitutionsFeatures.hasLicence userId, (error, hasLicence) -> - return callback error if error? - return callback(null, null) unless hasLicence - callback(null, Settings.institutionPlanCode) - - - hasLicence: (userId, callback = (error, hasLicence) ->) -> - InstitutionsGetter.getConfirmedInstitutions userId, (error, institutions) -> - return callback error if error? - - hasLicence = institutions.some (institution) -> - institution.licence and institution.licence != 'free' - - callback(null, hasLicence) diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee deleted file mode 100644 index 8a9de16482..0000000000 --- a/services/web/app/coffee/Features/Institutions/InstitutionsGetter.coffee +++ /dev/null @@ -1,19 +0,0 @@ -UserGetter = require '../User/UserGetter' -UserMembershipsHandler = require "../UserMembership/UserMembershipsHandler" -UserMembershipEntityConfigs = require "../UserMembership/UserMembershipEntityConfigs" -logger = require 'logger-sharelatex' - -module.exports = InstitutionsGetter = - getConfirmedInstitutions: (userId, callback = (error, institutions) ->) -> - UserGetter.getUserFullEmails userId, (error, emailsData) -> - return callback error if error? - - confirmedInstitutions = emailsData.filter (emailData) -> - emailData.confirmedAt? and emailData.affiliation?.institution?.confirmed - .map (emailData) -> - emailData.affiliation?.institution - - callback(null, confirmedInstitutions) - - getManagedInstitutions: (user_id, callback = (error, managedInstitutions) ->) -> - UserMembershipsHandler.getEntitiesByUser UserMembershipEntityConfigs.institution, user_id, callback diff --git a/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee b/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee deleted file mode 100644 index 73a230ea39..0000000000 --- a/services/web/app/coffee/Features/Institutions/InstitutionsManager.coffee +++ /dev/null @@ -1,103 +0,0 @@ -logger = require 'logger-sharelatex' -async = require 'async' -db = require("../../infrastructure/mongojs").db -_ = require("underscore") -ObjectId = require("../../infrastructure/mongojs").ObjectId -{ getInstitutionAffiliations } = require('./InstitutionsAPI') -FeaturesUpdater = require('../Subscription/FeaturesUpdater') -UserGetter = require('../User/UserGetter') -NotificationsBuilder = require("../Notifications/NotificationsBuilder") -SubscriptionLocator = require("../Subscription/SubscriptionLocator") -Institution = require("../../models/Institution").Institution -Subscription = require("../../models/Subscription").Subscription - -ASYNC_LIMIT = 10 -module.exports = InstitutionsManager = - upgradeInstitutionUsers: (institutionId, callback = (error) ->) -> - async.waterfall [ - (cb) -> - fetchInstitutionAndAffiliations institutionId, cb - (institution, affiliations, cb) -> - affiliations = _.map affiliations, (affiliation) -> - affiliation.institutionName = institution.name - affiliation.institutionId = institutionId - return affiliation - async.eachLimit affiliations, ASYNC_LIMIT, refreshFeatures, (err) -> cb(err) - ], callback - - checkInstitutionUsers: (institutionId, callback = (error) ->) -> - getInstitutionAffiliations institutionId, (error, affiliations) -> - UserGetter.getUsersByAnyConfirmedEmail( - affiliations.map((affiliation) -> affiliation.email), - { features: 1 }, - (error, users) -> - callback(error, checkFeatures(users)) - ) - - getInstitutionUsersSubscriptions: (institutionId, callback = (error, subscriptions) ->) -> - getInstitutionAffiliations institutionId, (error, affiliations) -> - return callback(error) if error? - userIds = affiliations.map (affiliation) -> ObjectId(affiliation.user_id) - Subscription - .find admin_id: userIds, planCode: { $not: /trial/ } - .populate 'admin_id', 'email' - .exec callback - -fetchInstitutionAndAffiliations = (institutionId, callback) -> - async.waterfall [ - (cb) -> - Institution.findOne {v1Id: institutionId}, (err, institution) -> cb(err, institution) - (institution, cb) -> - institution.fetchV1Data (err, institution) -> cb(err, institution) - (institution, cb) -> - getInstitutionAffiliations institutionId, (err, affiliations) -> cb(err, institution, affiliations) - ], callback - -refreshFeatures = (affiliation, callback) -> - userId = ObjectId(affiliation.user_id) - async.waterfall [ - (cb) -> - FeaturesUpdater.refreshFeatures userId, true, (err, features, featuresChanged) -> cb(err, featuresChanged) - (featuresChanged, cb) -> - getUserInfo userId, (error, user, subscription) -> cb(error, user, subscription, featuresChanged) - (user, subscription, featuresChanged, cb) -> - notifyUser user, affiliation, subscription, featuresChanged, cb - ], callback - -getUserInfo = (userId, callback) -> - async.waterfall [ - (cb) -> - UserGetter.getUser userId, cb - (user, cb) -> - SubscriptionLocator.getUsersSubscription user, (err, subscription) -> cb(err, user, subscription) - ], callback - -notifyUser = (user, affiliation, subscription, featuresChanged, callback) -> - async.parallel [ - (cb) -> - if featuresChanged - NotificationsBuilder.featuresUpgradedByAffiliation(affiliation, user).create cb - else - cb() - (cb) -> - if subscription? and !subscription.planCode.match(/(free|trial)/)? and !subscription.groupPlan - NotificationsBuilder.redundantPersonalSubscription(affiliation, user).create cb - else - cb() - ], callback - -checkFeatures = (users) -> - usersSummary = { - totalConfirmedUsers: users.length - totalConfirmedProUsers: 0 - totalConfirmedNonProUsers: 0 - confirmedNonProUsers: [] - } - users.forEach((user) -> - if user.features.collaborators == -1 and user.features.trackChanges - usersSummary.totalConfirmedProUsers += 1 - else - usersSummary.totalConfirmedNonProUsers += 1 - usersSummary.confirmedNonProUsers.push user._id - ) - return usersSummary diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee deleted file mode 100644 index 03c4e6c975..0000000000 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ /dev/null @@ -1,149 +0,0 @@ -AuthenticationController = require '../Authentication/AuthenticationController' -EditorController = require '../Editor/EditorController' -ProjectLocator = require '../Project/ProjectLocator' -Settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' -_ = require 'underscore' -LinkedFilesHandler = require './LinkedFilesHandler' -{ - UrlFetchFailedError, - InvalidUrlError, - OutputFileFetchFailedError, - AccessDeniedError, - BadEntityTypeError, - BadDataError, - ProjectNotFoundError, - V1ProjectNotFoundError, - SourceFileNotFoundError, - NotOriginalImporterError, - FeatureNotAvailableError, - RemoteServiceError, - FileCannotRefreshError -} = require './LinkedFilesErrors' -Modules = require '../../infrastructure/Modules' - - -module.exports = LinkedFilesController = { - - Agents: _.extend( - { - url: require('./UrlAgent'), - project_file: require('./ProjectFileAgent'), - project_output_file: require('./ProjectOutputFileAgent'), - }, - Modules.linkedFileAgentsIncludes() - ) - - _getAgent: (provider) -> - if !LinkedFilesController.Agents.hasOwnProperty(provider) - return null - unless provider in Settings.enabledLinkedFileTypes - return null - LinkedFilesController.Agents[provider] - - createLinkedFile: (req, res, next) -> - {project_id} = req.params - {name, provider, data, parent_folder_id} = req.body - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log {project_id, name, provider, data, parent_folder_id, user_id}, 'create linked file request' - - Agent = LinkedFilesController._getAgent(provider) - if !Agent? - return res.sendStatus(400) - - data.provider = provider - - Agent.createLinkedFile project_id, - data, - name, - parent_folder_id, - user_id, - (err, newFileId) -> - return LinkedFilesController.handleError(err, req, res, next) if err? - res.json(new_file_id: newFileId) - - refreshLinkedFile: (req, res, next) -> - {project_id, file_id} = req.params - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log {project_id, file_id, user_id}, 'refresh linked file request' - - LinkedFilesHandler.getFileById project_id, file_id, (err, file, path, parentFolder) -> - return next(err) if err? - return res.sendStatus(404) if !file? - name = file.name - linkedFileData = file.linkedFileData - if !linkedFileData? || !linkedFileData?.provider? - return res.send(409) - provider = linkedFileData.provider - parent_folder_id = parentFolder._id - Agent = LinkedFilesController._getAgent(provider) - if !Agent? - return res.sendStatus(400) - - Agent.refreshLinkedFile project_id, - linkedFileData, - name, - parent_folder_id, - user_id, - (err, newFileId) -> - return LinkedFilesController.handleError(err, req, res, next) if err? - res.json(new_file_id: newFileId) - - handleError: (error, req, res, next) -> - if error instanceof BadDataError - res.status(400).send("The submitted data is not valid") - - else if error instanceof AccessDeniedError - res.status(403).send("You do not have access to this project") - - else if error instanceof BadDataError - res.status(400).send("The submitted data is not valid") - - else if error instanceof BadEntityTypeError - res.status(400).send("The file is the wrong type") - - else if error instanceof SourceFileNotFoundError - res.status(404).send("Source file not found") - - else if error instanceof ProjectNotFoundError - res.status(404).send("Project not found") - - else if error instanceof V1ProjectNotFoundError - res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file") - - else if error instanceof OutputFileFetchFailedError - res.status(404).send("Could not get output file") - - else if error instanceof UrlFetchFailedError - res.status(422).send( - "Your URL could not be reached (#{error.statusCode} status code). Please check it and try again." - ) - - else if error instanceof InvalidUrlError - res.status(422).send( - "Your URL is not valid. Please check it and try again." - ) - - else if error instanceof NotOriginalImporterError - res.status(400).send( - "You are not the user who originally imported this file" - ) - - else if error instanceof FeatureNotAvailableError - res.status(400).send( - "This feature is not enabled on your account" - ) - - else if error instanceof RemoteServiceError - res.status(502).send( - "The remote service produced an error" - ) - - else if error instanceof FileCannotRefreshError - res.status(400).send( - "This file cannot be refreshed" - ) - - else - next(error) -} diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee deleted file mode 100644 index 96509c91f1..0000000000 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee +++ /dev/null @@ -1,120 +0,0 @@ -UrlFetchFailedError = (message) -> - error = new Error(message) - error.name = 'UrlFetchFailedError' - error.__proto__ = UrlFetchFailedError.prototype - return error -UrlFetchFailedError.prototype.__proto__ = Error.prototype - - -InvalidUrlError = (message) -> - error = new Error(message) - error.name = 'InvalidUrlError' - error.__proto__ = InvalidUrlError.prototype - return error -InvalidUrlError.prototype.__proto__ = Error.prototype - - -OutputFileFetchFailedError = (message) -> - error = new Error(message) - error.name = 'OutputFileFetchFailedError' - error.__proto__ = OutputFileFetchFailedError.prototype - return error -OutputFileFetchFailedError.prototype.__proto__ = Error.prototype - - -AccessDeniedError = (message) -> - error = new Error(message) - error.name = 'AccessDenied' - error.__proto__ = AccessDeniedError.prototype - return error -AccessDeniedError.prototype.__proto__ = Error.prototype - - -BadEntityTypeError = (message) -> - error = new Error(message) - error.name = 'BadEntityType' - error.__proto__ = BadEntityTypeError.prototype - return error -BadEntityTypeError.prototype.__proto__ = Error.prototype - - -BadDataError = (message) -> - error = new Error(message) - error.name = 'BadData' - error.__proto__ = BadDataError.prototype - return error -BadDataError.prototype.__proto__ = Error.prototype - - -ProjectNotFoundError = (message) -> - error = new Error(message) - error.name = 'ProjectNotFound' - error.__proto__ = ProjectNotFoundError.prototype - return error -ProjectNotFoundError.prototype.__proto__ = Error.prototype - - -V1ProjectNotFoundError = (message) -> - error = new Error(message) - error.name = 'V1ProjectNotFound' - error.__proto__ = V1ProjectNotFoundError.prototype - return error -V1ProjectNotFoundError.prototype.__proto__ = Error.prototype - - -SourceFileNotFoundError = (message) -> - error = new Error(message) - error.name = 'SourceFileNotFound' - error.__proto__ = SourceFileNotFoundError.prototype - return error -SourceFileNotFoundError.prototype.__proto__ = Error.prototype - - -NotOriginalImporterError = (message) -> - error = new Error(message) - error.name = 'NotOriginalImporter' - error.__proto__ = NotOriginalImporterError.prototype - return error -NotOriginalImporterError.prototype.__proto__ = Error.prototype - - -FeatureNotAvailableError = (message) -> - error = new Error(message) - error.name = 'FeatureNotAvailable' - error.__proto__ = FeatureNotAvailableError.prototype - return error -FeatureNotAvailableError.prototype.__proto__ = Error.prototype - - -RemoteServiceError = (message) -> - error = new Error(message) - error.name = 'RemoteService' - error.__proto__ = RemoteServiceError.prototype - return error -RemoteServiceError.prototype.__proto__ = Error.prototype - - -FileCannotRefreshError = (message) -> - error = new Error(message) - error.name = 'RemoteService' - error.__proto__ = FileCannotRefreshError.prototype - return error -FileCannotRefreshError.prototype.__proto__ = Error.prototype - - -module.exports = { - - UrlFetchFailedError, - InvalidUrlError, - OutputFileFetchFailedError, - AccessDeniedError, - BadEntityTypeError, - BadDataError, - ProjectNotFoundError, - V1ProjectNotFoundError, - SourceFileNotFoundError, - NotOriginalImporterError, - FeatureNotAvailableError, - RemoteServiceError, - FileCannotRefreshError -} diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee deleted file mode 100644 index 6262f0a5ab..0000000000 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee +++ /dev/null @@ -1,86 +0,0 @@ -FileWriter = require '../../infrastructure/FileWriter' -EditorController = require '../Editor/EditorController' -ProjectLocator = require '../Project/ProjectLocator' -Project = require("../../models/Project").Project -ProjectGetter = require("../Project/ProjectGetter") -_ = require 'underscore' -{ - ProjectNotFoundError, - V1ProjectNotFoundError, - BadDataError -} = require './LinkedFilesErrors' - - -module.exports = LinkedFilesHandler = - - getFileById: (project_id, file_id, callback=(err, file)->) -> - ProjectLocator.findElement { - project_id, - element_id: file_id, - type: 'file' - }, (err, file, path, parentFolder) -> - return callback(err) if err? - callback(null, file, path, parentFolder) - - getSourceProject: (data, callback=(err, project)->) -> - projection = {_id: 1, name: 1} - if data.v1_source_doc_id? - Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) -> - return callback(err) if err? - if !project? - return callback(new V1ProjectNotFoundError()) - callback(null, project) - else if data.source_project_id? - ProjectGetter.getProject data.source_project_id, projection, (err, project) -> - return callback(err) if err? - if !project? - return callback(new ProjectNotFoundError()) - callback(null, project) - else - callback(new BadDataError('neither v1 nor v2 id present')) - - importFromStream: ( - project_id, - readStream, - linkedFileData, - name, - parent_folder_id, - user_id, - callback=(err, file)-> - ) -> - callback = _.once(callback) - FileWriter.writeStreamToDisk project_id, readStream, (err, fsPath) -> - return callback(err) if err? - EditorController.upsertFile project_id, - parent_folder_id, - name, - fsPath, - linkedFileData, - "upload", - user_id, - (err, file) => - return callback(err) if err? - callback(null, file) - - importContent: ( - project_id, - content, - linkedFileData, - name, - parent_folder_id, - user_id, - callback=(err, file)-> - ) -> - callback = _.once(callback) - FileWriter.writeContentToDisk project_id, content, (err, fsPath) -> - return callback(err) if err? - EditorController.upsertFile project_id, - parent_folder_id, - name, - fsPath, - linkedFileData, - "upload", - user_id, - (err, file) => - return callback(err) if err? - callback(null, file) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesRouter.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesRouter.coffee deleted file mode 100644 index 0d2bb89280..0000000000 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesRouter.coffee +++ /dev/null @@ -1,28 +0,0 @@ -AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') -AuthenticationController = require('../Authentication/AuthenticationController') -RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') -LinkedFilesController = require "./LinkedFilesController" - -module.exports = - apply: (webRouter) -> - webRouter.post '/project/:project_id/linked_file', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - RateLimiterMiddleware.rateLimit({ - endpointName: "create-linked-file" - params: ["project_id"] - maxRequests: 100 - timeInterval: 60 - }), - LinkedFilesController.createLinkedFile - - webRouter.post '/project/:project_id/linked_file/:file_id/refresh', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - RateLimiterMiddleware.rateLimit({ - endpointName: "refresh-linked-file" - params: ["project_id"] - maxRequests: 100 - timeInterval: 60 - }), - LinkedFilesController.refreshLinkedFile diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee deleted file mode 100644 index ed83379a60..0000000000 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ /dev/null @@ -1,122 +0,0 @@ -AuthorizationManager = require('../Authorization/AuthorizationManager') -ProjectLocator = require('../Project/ProjectLocator') -ProjectGetter = require('../Project/ProjectGetter') -DocstoreManager = require('../Docstore/DocstoreManager') -DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') -FileStoreHandler = require('../FileStore/FileStoreHandler') -_ = require "underscore" -Settings = require 'settings-sharelatex' -LinkedFilesHandler = require './LinkedFilesHandler' -{ - BadDataError, - AccessDeniedError, - BadEntityTypeError, - SourceFileNotFoundError, - ProjectNotFoundError, - V1ProjectNotFoundError -} = require './LinkedFilesErrors' - -module.exports = ProjectFileAgent = { - - createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> - if !@_canCreate(linkedFileData) - return callback(new AccessDeniedError()) - @_go(project_id, linkedFileData, name, parent_folder_id, user_id, callback) - - refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> - @_go project_id, linkedFileData, name, parent_folder_id, user_id, callback - - _prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) -> - @_checkAuth project_id, linkedFileData, user_id, (err, allowed) => - return callback(err) if err? - return callback(new AccessDeniedError()) if !allowed - if !@_validate(linkedFileData) - return callback(new BadDataError()) - callback(null, linkedFileData) - - _go: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> - linkedFileData = @_sanitizeData(linkedFileData) - @_prepare project_id, linkedFileData, user_id, (err, linkedFileData) => - return callback(err) if err? - if !@_validate(linkedFileData) - return callback(new BadDataError()) - @_getEntity linkedFileData, user_id, (err, source_project, entity, type) => - return callback(err) if err? - if type == 'doc' - DocstoreManager.getDoc source_project._id, entity._id, (err, lines) -> - return callback(err) if err? - LinkedFilesHandler.importContent project_id, - lines.join('\n'), - linkedFileData, - name, - parent_folder_id, - user_id, - (err, file) -> - return callback(err) if err? - callback(null, file._id) # Created - else if type == 'file' - FileStoreHandler.getFileStream source_project._id, entity._id, null, (err, fileStream) -> - return callback(err) if err? - LinkedFilesHandler.importFromStream project_id, - fileStream, - linkedFileData, - name, - parent_folder_id, - user_id, - (err, file) -> - return callback(err) if err? - callback(null, file._id) # Created - else - callback(new BadEntityTypeError()) - - _getEntity: - (linkedFileData, current_user_id, callback = (err, entity, type) ->) -> - callback = _.once(callback) - { source_entity_path } = linkedFileData - @_getSourceProject linkedFileData, (err, project) -> - return callback(err) if err? - source_project_id = project._id - DocumentUpdaterHandler.flushProjectToMongo source_project_id, (err) -> - return callback(err) if err? - ProjectLocator.findElementByPath { - project_id: source_project_id, - path: source_entity_path - exactCaseMatch: true - }, (err, entity, type) -> - if err? - if /^not found.*/.test(err.toString()) - err = new SourceFileNotFoundError() - return callback(err) - callback(null, project, entity, type) - - _sanitizeData: (data) -> - return _.pick( - data, - 'provider', - 'source_project_id', - 'v1_source_doc_id', - 'source_entity_path' - ) - - _validate: (data) -> - return ( - (data.source_project_id? || data.v1_source_doc_id?) && - data.source_entity_path? - ) - - _canCreate: (data) -> - # Don't allow creation of linked-files with v1 doc ids - !data.v1_source_doc_id? - - _getSourceProject: LinkedFilesHandler.getSourceProject - - _checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> - callback = _.once(callback) - if !ProjectFileAgent._validate(data) - return callback(new BadDataError()) - @_getSourceProject data, (err, project) -> - return callback(err) if err? - AuthorizationManager.canUserReadProject current_user_id, project._id, null, (err, canRead) -> - return callback(err) if err? - callback(null, canRead) -} diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee deleted file mode 100644 index e6a5392ba2..0000000000 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ /dev/null @@ -1,164 +0,0 @@ -AuthorizationManager = require('../Authorization/AuthorizationManager') -ProjectGetter = require('../Project/ProjectGetter') -Settings = require 'settings-sharelatex' -CompileManager = require '../Compile/CompileManager' -ClsiManager = require '../Compile/ClsiManager' -ProjectFileAgent = require './ProjectFileAgent' -_ = require "underscore" -{ - BadDataError, - AccessDeniedError, - BadEntityTypeError, - OutputFileFetchFailedError -} = require './LinkedFilesErrors' -LinkedFilesHandler = require './LinkedFilesHandler' -logger = require 'logger-sharelatex' - - -module.exports = ProjectOutputFileAgent = { - - _prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) -> - @_checkAuth project_id, linkedFileData, user_id, (err, allowed) => - return callback(err) if err? - return callback(new AccessDeniedError()) if !allowed - if !@_validate(linkedFileData) - return callback(new BadDataError()) - callback(null, linkedFileData) - - createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> - if !@_canCreate(linkedFileData) - return callback(new AccessDeniedError()) - linkedFileData = @_sanitizeData(linkedFileData) - @_prepare project_id, linkedFileData, user_id, (err, linkedFileData) => - return callback(err) if err? - @_getFileStream linkedFileData, user_id, (err, readStream) => - return callback(err) if err? - readStream.on "error", callback - readStream.on "response", (response) => - if 200 <= response.statusCode < 300 - readStream.resume() - LinkedFilesHandler.importFromStream project_id, - readStream, - linkedFileData, - name, - parent_folder_id, - user_id, - (err, file) -> - return callback(err) if err? - callback(null, file._id) # Created - else - err = new OutputFileFetchFailedError( - "Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}" - ) - err.statusCode = response.statusCode - callback(err) - - refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> - @_prepare project_id, linkedFileData, user_id, (err, linkedFileData) => - return callback(err) if err? - @_compileAndGetFileStream linkedFileData, user_id, (err, readStream, new_build_id) => - return callback(err) if err? - readStream.on "error", callback - readStream.on "response", (response) => - if 200 <= response.statusCode < 300 - readStream.resume() - linkedFileData.build_id = new_build_id - LinkedFilesHandler.importFromStream project_id, - readStream, - linkedFileData, - name, - parent_folder_id, - user_id, - (err, file) -> - return callback(err) if err? - callback(null, file._id) # Created - else - err = new OutputFileFetchFailedError( - "Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}" - ) - err.statusCode = response.statusCode - callback(err) - - - _sanitizeData: (data) -> - return { - provider: data.provider, - source_project_id: data.source_project_id, - source_output_file_path: data.source_output_file_path, - build_id: data.build_id - } - - _canCreate: ProjectFileAgent._canCreate - - _getSourceProject: LinkedFilesHandler.getSourceProject - - _validate: (data) -> - if data.v1_source_doc_id? - ( - data.v1_source_doc_id? && - data.source_output_file_path? - ) - else - ( - data.source_project_id? && - data.source_output_file_path? && - data.build_id? - ) - - _checkAuth: (project_id, data, current_user_id, callback = (err, allowed)->) -> - callback = _.once(callback) - if !@_validate(data) - return callback(new BadDataError()) - @_getSourceProject data, (err, project) -> - return callback(err) if err? - AuthorizationManager.canUserReadProject current_user_id, - project._id, - null, - (err, canRead) -> - return callback(err) if err? - callback(null, canRead) - - _getFileStream: (linkedFileData, user_id, callback=(err, fileStream)->) -> - callback = _.once(callback) - { source_output_file_path, build_id } = linkedFileData - @_getSourceProject linkedFileData, (err, project) -> - return callback(err) if err? - source_project_id = project._id - ClsiManager.getOutputFileStream source_project_id, - user_id, - build_id, - source_output_file_path, - (err, readStream) -> - return callback(err) if err? - readStream.pause() - callback(null, readStream) - - _compileAndGetFileStream: (linkedFileData, user_id, callback=(err, stream, build_id)->) -> - callback = _.once(callback) - { source_output_file_path } = linkedFileData - @_getSourceProject linkedFileData, (err, project) -> - return callback(err) if err? - source_project_id = project._id - CompileManager.compile source_project_id, - user_id, - {}, - (err, status, outputFiles) -> - return callback(err) if err? - if status != 'success' - return callback(new OutputFileFetchFailedError()) - outputFile = _.find( - outputFiles, - (o) => o.path == source_output_file_path - ) - if !outputFile? - return callback(new OutputFileFetchFailedError()) - build_id = outputFile.build - ClsiManager.getOutputFileStream source_project_id, - user_id, - build_id, - source_output_file_path, - (err, readStream) -> - return callback(err) if err? - readStream.pause() - callback(null, readStream, build_id) -} diff --git a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee deleted file mode 100644 index e9fe564009..0000000000 --- a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee +++ /dev/null @@ -1,51 +0,0 @@ -request = require 'request' -_ = require "underscore" -urlValidator = require 'valid-url' -{ InvalidUrlError, UrlFetchFailedError } = require './LinkedFilesErrors' -LinkedFilesHandler = require './LinkedFilesHandler' -UrlHelper = require '../Helpers/UrlHelper' - -module.exports = UrlAgent = { - - createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> - linkedFileData = @._sanitizeData(linkedFileData) - @_getUrlStream project_id, linkedFileData, user_id, (err, readStream) -> - return callback(err) if err? - readStream.on "error", callback - readStream.on "response", (response) -> - if 200 <= response.statusCode < 300 - readStream.resume() - LinkedFilesHandler.importFromStream project_id, - readStream, - linkedFileData, - name, - parent_folder_id, - user_id, - (err, file) -> - return callback(err) if err? - callback(null, file._id) # Created - else - error = new UrlFetchFailedError("url fetch failed: #{linkedFileData.url}") - error.statusCode = response.statusCode - callback(error) - - refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> - @createLinkedFile project_id, linkedFileData, name, parent_folder_id, user_id, callback - - _sanitizeData: (data) -> - return { - provider: data.provider - url: UrlHelper.prependHttpIfNeeded(data.url) - } - - _getUrlStream: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> - callback = _.once(callback) - url = data.url - if !urlValidator.isWebUri(url) - return callback(new InvalidUrlError("invalid url: #{url}")) - url = UrlHelper.wrapUrlWithProxy(url) - readStream = request.get(url) - readStream.pause() - callback(null, readStream) - -} diff --git a/services/web/app/coffee/Features/Metadata/MetaController.coffee b/services/web/app/coffee/Features/Metadata/MetaController.coffee deleted file mode 100644 index 21320d4d25..0000000000 --- a/services/web/app/coffee/Features/Metadata/MetaController.coffee +++ /dev/null @@ -1,28 +0,0 @@ -EditorRealTimeController = require "../Editor/EditorRealTimeController" -MetaHandler = require './MetaHandler' -logger = require 'logger-sharelatex' - - -module.exports = MetaController = - - getMetadata: (req, res, next) -> - project_id = req.params.project_id - logger.log {project_id}, "getting all labels for project" - MetaHandler.getAllMetaForProject project_id, (err, projectMeta) -> - if err? - logger.err {project_id, err}, "[MetaController] error getting all labels from project" - return next err - res.json {projectId: project_id, projectMeta: projectMeta} - - broadcastMetadataForDoc: (req, res, next) -> - project_id = req.params.project_id - doc_id = req.params.doc_id - logger.log {project_id, doc_id}, "getting labels for doc" - MetaHandler.getMetaForDoc project_id, doc_id, (err, docMeta) -> - if err? - logger.err {project_id, doc_id, err}, "[MetaController] error getting labels from doc" - return next err - EditorRealTimeController.emitToRoom project_id, 'broadcastDocMeta', { - docId: doc_id, meta: docMeta - } - res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Metadata/MetaHandler.coffee b/services/web/app/coffee/Features/Metadata/MetaHandler.coffee deleted file mode 100644 index 822bdbde07..0000000000 --- a/services/web/app/coffee/Features/Metadata/MetaHandler.coffee +++ /dev/null @@ -1,66 +0,0 @@ -ProjectEntityHandler = require "../Project/ProjectEntityHandler" -DocumentUpdaterHandler = require '../DocumentUpdater/DocumentUpdaterHandler' -packageMapping = require "./packageMapping" - - -module.exports = MetaHandler = - - labelRegex: () -> - /\\label{(.{0,80}?)}/g - - usepackageRegex: () -> - /^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/g - - ReqPackageRegex: () -> - /^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/g - - getAllMetaForProject: (projectId, callback=(err, projectMeta)->) -> - DocumentUpdaterHandler.flushProjectToMongo projectId, (err) -> - if err? - return callback err - ProjectEntityHandler.getAllDocs projectId, (err, docs) -> - if err? - return callback err - projectMeta = MetaHandler.extractMetaFromProjectDocs docs - callback null, projectMeta - - getMetaForDoc: (projectId, docId, callback=(err, docMeta)->) -> - DocumentUpdaterHandler.flushDocToMongo projectId, docId, (err) -> - if err? - return callback err - ProjectEntityHandler.getDoc projectId, docId, (err, lines) -> - if err? - return callback err - docMeta = MetaHandler.extractMetaFromDoc lines - callback null, docMeta - - extractMetaFromDoc: (lines) -> - docMeta = {labels: [], packages: {}} - packages = [] - label_re = MetaHandler.labelRegex() - package_re = MetaHandler.usepackageRegex() - req_package_re = MetaHandler.ReqPackageRegex() - for line in lines - while labelMatch = label_re.exec line - if label = labelMatch[1] - docMeta.labels.push label - while packageMatch = package_re.exec line - if messy = packageMatch[1] - for pkg in messy.split ',' - if clean = pkg.trim() - packages.push clean - while packageMatch = req_package_re.exec line - if messy = packageMatch[1] - for pkg in messy.split ',' - if clean = pkg.trim() - packages.push clean - for pkg in packages - if packageMapping[pkg]? - docMeta.packages[pkg] = packageMapping[pkg] - return docMeta - - extractMetaFromProjectDocs: (projectDocs) -> - projectMeta = {} - for _path, doc of projectDocs - projectMeta[doc._id] = MetaHandler.extractMetaFromDoc doc.lines - return projectMeta diff --git a/services/web/app/coffee/Features/Metadata/packageMapping.coffee b/services/web/app/coffee/Features/Metadata/packageMapping.coffee deleted file mode 100644 index e2bd07b8bc..0000000000 --- a/services/web/app/coffee/Features/Metadata/packageMapping.coffee +++ /dev/null @@ -1 +0,0 @@ -module.exports = {"inputenc": [{"caption": "\\inputencoding{}", "snippet": "\\inputencoding{$1}", "meta": "inputenc-cmd", "score": 0.0002447047447770061}], "graphicx": [{"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "graphicx-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "graphicx-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "graphicx-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "graphicx-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "graphicx-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "graphicx-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "graphicx-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "graphicx-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "graphicx-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "graphicx-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "graphicx-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "graphicx-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "graphicx-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "graphicx-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "graphicx-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "graphicx-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "graphicx-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "graphicx-cmd", "score": 0.004649150613625593}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "graphicx-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "graphicx-cmd", "score": 0.008565354665444157}], "amsmath": [{"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "amsmath-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "amsmath-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "amsmath-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "amsmath-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "amsmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "amsmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "amsmath-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "amsmath-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "amsmath-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "amsmath-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "amsmath-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "amsmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "amsmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "amsmath-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "amsmath-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "amsmath-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "amsmath-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "amsmath-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "amsmath-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "amsmath-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "amsmath-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "amsmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "amsmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "amsmath-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "amsmath-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "amsmath-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "amsmath-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "amsmath-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "amsmath-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "amsmath-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "amsmath-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "amsmath-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "amsmath-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "amsmath-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "amsmath-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "amsmath-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "amsmath-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "amsmath-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "amsmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "amsmath-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "amsmath-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "amsmath-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "amsmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "amsmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "amsmath-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "amsmath-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "amsmath-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "amsmath-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "amsmath-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "amsmath-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "amsmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "amsmath-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "amsmath-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "amsmath-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "amsmath-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "amsmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "amsmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "amsmath-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "amsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "amsmath-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "amsmath-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "amsmath-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "amsmath-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "amsmath-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "amsmath-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "amsmath-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "amsmath-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "amsmath-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "amsmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "amsmath-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "amsmath-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "amsmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "amsmath-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "amsmath-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "amsmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "amsmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "amsmath-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "amsmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "amsmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "amsmath-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "amsmath-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "amsmath-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "amsmath-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "amsmath-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "amsmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "amsmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "amsmath-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "amsmath-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "amsmath-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "amsmath-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "amsmath-cmd", "score": 0.0058847868741168765}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "amsmath-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "amsmath-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "amsmath-cmd", "score": 0.18137737738638837}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "amsmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "amsmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "amsmath-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "amsmath-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "amsmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "amsmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "amsmath-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "amsmath-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "amsmath-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "amsmath-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "amsmath-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "amsmath-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "amsmath-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "amsmath-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "amsmath-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "amsmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "amsmath-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "amsmath-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "amsmath-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "amsmath-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "amsmath-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "amsmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "amsmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "amsmath-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "amsmath-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "amsmath-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "amsmath-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "amsmath-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "amsmath-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "amsmath-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "amsmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "amsmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "amsmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "amsmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "amsmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "amsmath-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "amsmath-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "amsmath-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "amsmath-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "amsmath-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "amsmath-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "amsmath-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "amsmath-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "amsmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "amsmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "amsmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "amsmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "amsmath-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "amsmath-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "amsmath-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "amsmath-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "amsmath-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "amsmath-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "amsmath-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "amsmath-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "amsmath-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "amsmath-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "amsmath-cmd", "score": 0.0063276692758974925}], "geometry": [{"caption": "\\savegeometry{}", "snippet": "\\savegeometry{$1}", "meta": "geometry-cmd", "score": 6.461638865465447e-05}, {"caption": "\\loadgeometry{}", "snippet": "\\loadgeometry{$1}", "meta": "geometry-cmd", "score": 6.461638865465447e-05}, {"caption": "\\newgeometry{}", "snippet": "\\newgeometry{$1}", "meta": "geometry-cmd", "score": 0.0025977479207639352}, {"caption": "\\geometry{}", "snippet": "\\geometry{$1}", "meta": "geometry-cmd", "score": 0.046218420429973615}, {"caption": "\\csname", "snippet": "\\csname", "meta": "geometry-cmd", "score": 0.008565354665444157}, {"caption": "\\restoregeometry", "snippet": "\\restoregeometry", "meta": "geometry-cmd", "score": 0.0007546303842143648}, {"caption": "\\csname", "snippet": "\\csname", "meta": "geometry-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "geometry-cmd", "score": 0.002958865219480927}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "geometry-cmd", "score": 0.00037306820619479756}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "geometry-cmd", "score": 0.00021116765384691477}], "amssymb": [{"caption": "\\frak{}", "snippet": "\\frak{$1}", "meta": "amssymb-cmd", "score": 0.0017966000518546787}, {"caption": "\\checkmark", "snippet": "\\checkmark", "meta": "amssymb-cmd", "score": 0.025060530944368123}, {"caption": "\\bold", "snippet": "\\bold", "meta": "amssymb-cmd", "score": 0.0014358547624941567}, {"caption": "\\bold{}", "snippet": "\\bold{$1}", "meta": "amssymb-cmd", "score": 0.0014358547624941567}, {"caption": "\\Bbb{}", "snippet": "\\Bbb{$1}", "meta": "amssymb-cmd", "score": 0.0006671850995492977}, {"caption": "\\Bbb", "snippet": "\\Bbb", "meta": "amssymb-cmd", "score": 0.0006671850995492977}], "hyperref": [{"caption": "\\nameref{}", "snippet": "\\nameref{$1}", "meta": "hyperref-cmd", "score": 0.009472569279662113}, {"caption": "\\pdfbookmark[]{}{}", "snippet": "\\pdfbookmark[$1]{$2}{$3}", "meta": "hyperref-cmd", "score": 0.006492248863367502}, {"caption": "\\figureautorefname", "snippet": "\\figureautorefname", "meta": "hyperref-cmd", "score": 0.00014582556188448738}, {"caption": "\\figureautorefname{}", "snippet": "\\figureautorefname{$1}", "meta": "hyperref-cmd", "score": 0.00014582556188448738}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "hyperref-cmd", "score": 0.006963729684667191}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\footnoteautorefname", "snippet": "\\footnoteautorefname", "meta": "hyperref-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\roman{}", "snippet": "\\roman{$1}", "meta": "hyperref-cmd", "score": 0.005553384455935491}, {"caption": "\\roman", "snippet": "\\roman", "meta": "hyperref-cmd", "score": 0.005553384455935491}, {"caption": "\\string", "snippet": "\\string", "meta": "hyperref-cmd", "score": 0.001042697111754002}, {"caption": "\\MakeLowercase{}", "snippet": "\\MakeLowercase{$1}", "meta": "hyperref-cmd", "score": 0.017289599800633146}, {"caption": "\\textunderscore", "snippet": "\\textunderscore", "meta": "hyperref-cmd", "score": 0.001509072212764015}, {"caption": "\\do", "snippet": "\\do", "meta": "hyperref-cmd", "score": 0.009278344180101056}, {"caption": "\\begin{}", "snippet": "\\begin{$1}", "meta": "hyperref-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}[]", "snippet": "\\begin{$1}[$2]", "meta": "hyperref-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}{}", "snippet": "\\begin{$1}{$2}", "meta": "hyperref-cmd", "score": 7.849662248028187}, {"caption": "\\FancyVerbLineautorefname", "snippet": "\\FancyVerbLineautorefname", "meta": "hyperref-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\hyperlink{}{}", "snippet": "\\hyperlink{$1}{$2}", "meta": "hyperref-cmd", "score": 0.00978652043902115}, {"caption": "\\tableautorefname", "snippet": "\\tableautorefname", "meta": "hyperref-cmd", "score": 0.00012704528567339081}, {"caption": "\\tableautorefname{}", "snippet": "\\tableautorefname{$1}", "meta": "hyperref-cmd", "score": 0.00012704528567339081}, {"caption": "\\equationautorefname", "snippet": "\\equationautorefname", "meta": "hyperref-cmd", "score": 0.00018777198999871106}, {"caption": "\\equationautorefname{}", "snippet": "\\equationautorefname{$1}", "meta": "hyperref-cmd", "score": 0.00018777198999871106}, {"caption": "\\chapterautorefname", "snippet": "\\chapterautorefname", "meta": "hyperref-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\TeX", "snippet": "\\TeX", "meta": "hyperref-cmd", "score": 0.02873756018238537}, {"caption": "\\TeX{}", "snippet": "\\TeX{$1}", "meta": "hyperref-cmd", "score": 0.02873756018238537}, {"caption": "\\protect", "snippet": "\\protect", "meta": "hyperref-cmd", "score": 0.0200686676229443}, {"caption": "\\appendixautorefname", "snippet": "\\appendixautorefname", "meta": "hyperref-cmd", "score": 7.950698053641679e-05}, {"caption": "\\appendixautorefname{}", "snippet": "\\appendixautorefname{$1}", "meta": "hyperref-cmd", "score": 7.950698053641679e-05}, {"caption": "\\newlabel{}{}", "snippet": "\\newlabel{$1}{$2}", "meta": "hyperref-cmd", "score": 0.00029737672328168955}, {"caption": "\\texorpdfstring{}{}", "snippet": "\\texorpdfstring{$1}{$2}", "meta": "hyperref-cmd", "score": 0.0073781967296121}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "hyperref-cmd", "score": 0.002140559856649122}, {"caption": "\\alph", "snippet": "\\alph", "meta": "hyperref-cmd", "score": 0.01034327266194849}, {"caption": "\\alph{}", "snippet": "\\alph{$1}", "meta": "hyperref-cmd", "score": 0.01034327266194849}, {"caption": "\\pageref{}", "snippet": "\\pageref{$1}", "meta": "hyperref-cmd", "score": 0.019788865471151957}, {"caption": "\\item", "snippet": "\\item", "meta": "hyperref-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "hyperref-cmd", "score": 3.800886892251021}, {"caption": "\\LaTeX", "snippet": "\\LaTeX", "meta": "hyperref-cmd", "score": 0.2334089308452787}, {"caption": "\\LaTeX{}", "snippet": "\\LaTeX{$1}", "meta": "hyperref-cmd", "score": 0.2334089308452787}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\itemautorefname", "snippet": "\\itemautorefname", "meta": "hyperref-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "hyperref-cmd", "score": 1.2569477427490174}, {"caption": "\\sectionautorefname", "snippet": "\\sectionautorefname", "meta": "hyperref-cmd", "score": 0.0019832324299155183}, {"caption": "\\sectionautorefname{}", "snippet": "\\sectionautorefname{$1}", "meta": "hyperref-cmd", "score": 0.0019832324299155183}, {"caption": "\\LaTeXe", "snippet": "\\LaTeXe", "meta": "hyperref-cmd", "score": 0.007928096378157487}, {"caption": "\\LaTeXe{}", "snippet": "\\LaTeXe{$1}", "meta": "hyperref-cmd", "score": 0.007928096378157487}, {"caption": "\\footref{}", "snippet": "\\footref{$1}", "meta": "hyperref-cmd", "score": 0.0003680857021151614}, {"caption": "\\footref", "snippet": "\\footref", "meta": "hyperref-cmd", "score": 0.0003680857021151614}, {"caption": "\\hypertarget{}{}", "snippet": "\\hypertarget{$1}{$2}", "meta": "hyperref-cmd", "score": 0.009652820108904094}, {"caption": "\\theoremautorefname", "snippet": "\\theoremautorefname", "meta": "hyperref-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "hyperref-cmd", "score": 0.7504160124360846}, {"caption": "\\subparagraphautorefname", "snippet": "\\subparagraphautorefname", "meta": "hyperref-cmd", "score": 0.0005446476945175932}, {"caption": "\\url{}", "snippet": "\\url{$1}", "meta": "hyperref-cmd", "score": 0.13586474005868793}, {"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "hyperref-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "hyperref-cmd", "score": 0.8973590434087177}, {"caption": "\\href{}{}", "snippet": "\\href{$1}{$2}", "meta": "hyperref-cmd", "score": 0.27111130260612365}, {"caption": "\\Roman{}", "snippet": "\\Roman{$1}", "meta": "hyperref-cmd", "score": 0.0038703587462843594}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hyperref-cmd", "score": 0.00530510025314411}, {"caption": "\\autoref{}", "snippet": "\\autoref{$1}", "meta": "hyperref-cmd", "score": 0.03741172773691362}, {"caption": "\\nolinkurl{}", "snippet": "\\nolinkurl{$1}", "meta": "hyperref-cmd", "score": 0.0004995635515943437}, {"caption": "\\end{}", "snippet": "\\end{$1}", "meta": "hyperref-cmd", "score": 7.847906405228455}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "hyperref-cmd", "score": 0.0174633138331273}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "hyperref-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "hyperref-cmd", "score": 0.006776001543888959}, {"caption": "\\partautorefname", "snippet": "\\partautorefname", "meta": "hyperref-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\Itemautorefname{}", "snippet": "\\Itemautorefname{$1}", "meta": "hyperref-cmd", "score": 6.006262128895586e-05}, {"caption": "\\halign{}", "snippet": "\\halign{$1}", "meta": "hyperref-cmd", "score": 0.00017906650306643613}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "hyperref-cmd", "score": 0.20852115286477566}, {"caption": "\\ref{}", "snippet": "\\ref{$1}", "meta": "hyperref-cmd", "score": 1.4380093454211778}, {"caption": "\\Alph{}", "snippet": "\\Alph{$1}", "meta": "hyperref-cmd", "score": 0.002233258780143355}, {"caption": "\\Alph", "snippet": "\\Alph", "meta": "hyperref-cmd", "score": 0.002233258780143355}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "hyperref-cmd", "score": 0.047007158741781095}, {"caption": "\\MP", "snippet": "\\MP", "meta": "hyperref-cmd", "score": 0.00018344383742255004}, {"caption": "\\MP{}", "snippet": "\\MP{$1}", "meta": "hyperref-cmd", "score": 0.00018344383742255004}, {"caption": "\\paragraphautorefname", "snippet": "\\paragraphautorefname", "meta": "hyperref-cmd", "score": 0.0005446476945175932}, {"caption": "\\citeN{}", "snippet": "\\citeN{$1}", "meta": "hyperref-cmd", "score": 0.0018503938529945614}, {"caption": "\\citeN", "snippet": "\\citeN", "meta": "hyperref-cmd", "score": 0.0018503938529945614}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "hyperref-cmd", "score": 0.07503475348393239}, {"caption": "\\subsectionautorefname", "snippet": "\\subsectionautorefname", "meta": "hyperref-cmd", "score": 0.0012546605780895737}, {"caption": "\\subsectionautorefname{}", "snippet": "\\subsectionautorefname{$1}", "meta": "hyperref-cmd", "score": 0.0012546605780895737}, {"caption": "\\hyperref[]{}", "snippet": "\\hyperref[$1]{$2}", "meta": "hyperref-cmd", "score": 0.004515152477030062}, {"caption": "\\arabic{}", "snippet": "\\arabic{$1}", "meta": "hyperref-cmd", "score": 0.02445837629741638}, {"caption": "\\arabic", "snippet": "\\arabic", "meta": "hyperref-cmd", "score": 0.02445837629741638}, {"caption": "\\newline", "snippet": "\\newline", "meta": "hyperref-cmd", "score": 0.3311721696201715}, {"caption": "\\hypersetup{}", "snippet": "\\hypersetup{$1}", "meta": "hyperref-cmd", "score": 0.06967310843464661}, {"caption": "\\subsubsectionautorefname", "snippet": "\\subsubsectionautorefname", "meta": "hyperref-cmd", "score": 0.0012064581899162352}, {"caption": "\\subsubsectionautorefname{}", "snippet": "\\subsubsectionautorefname{$1}", "meta": "hyperref-cmd", "score": 0.0012064581899162352}, {"caption": "\\title{}", "snippet": "\\title{$1}", "meta": "hyperref-cmd", "score": 0.9202908262245683}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "hyperref-cmd", "score": 0.00037306820619479756}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hyperref-cmd", "score": 0.00530510025314411}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "hyperref-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "hyperref-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "hyperref-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "hyperref-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "hyperref-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "hyperref-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "hyperref-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "hyperref-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "hyperref-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "hyperref-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "hyperref-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "hyperref-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "hyperref-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hyperref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hyperref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hyperref-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "hyperref-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "hyperref-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hyperref-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hyperref-cmd", "score": 0.002958865219480927}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "hyperref-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hyperref-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hyperref-cmd", "score": 0.00530510025314411}], "babel": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "babel-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "babel-cmd", "score": 0.021170869458413965}], "color": [{"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "color-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "color-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "color-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "color-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "color-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "color-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "color-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "color-cmd", "score": 0.2864294797053033}], "xcolor": [{"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "xcolor-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xcolor-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xcolor-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "xcolor-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "xcolor-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "xcolor-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "xcolor-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "xcolor-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xcolor-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "xcolor-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "xcolor-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xcolor-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "xcolor-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "xcolor-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xcolor-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xcolor-cmd", "score": 0.2864294797053033}], "natbib": [{"caption": "\\citealt{}", "snippet": "\\citealt{$1}", "meta": "natbib-cmd", "score": 0.007302105441724955}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "natbib-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "natbib-cmd", "score": 0.021170869458413965}, {"caption": "\\textsuperscript{}", "snippet": "\\textsuperscript{$1}", "meta": "natbib-cmd", "score": 0.05216393882408519}, {"caption": "\\nocite{}", "snippet": "\\nocite{$1}", "meta": "natbib-cmd", "score": 0.04990693820960752}, {"caption": "\\bibname", "snippet": "\\bibname", "meta": "natbib-cmd", "score": 0.007599529252128519}, {"caption": "\\bibname{}", "snippet": "\\bibname{$1}", "meta": "natbib-cmd", "score": 0.007599529252128519}, {"caption": "\\bibpunct", "snippet": "\\bibpunct", "meta": "natbib-cmd", "score": 0.001148574749873469}, {"caption": "\\bibpunct{}{}{}{}{}{}", "snippet": "\\bibpunct{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "natbib-cmd", "score": 0.001148574749873469}, {"caption": "\\bibpunct[]{}{}{}{}{}{}", "snippet": "\\bibpunct[$1]{$2}{$3}{$4}{$5}{$6}{$7}", "meta": "natbib-cmd", "score": 0.001148574749873469}, {"caption": "\\citepalias{}", "snippet": "\\citepalias{$1}", "meta": "natbib-cmd", "score": 0.00032712684909035603}, {"caption": "\\citepalias[][]{}", "snippet": "\\citepalias[$1][$2]{$3}", "meta": "natbib-cmd", "score": 0.00032712684909035603}, {"caption": "\\makeindex", "snippet": "\\makeindex", "meta": "natbib-cmd", "score": 0.010304996748556729}, {"caption": "\\citep{}", "snippet": "\\citep{$1}", "meta": "natbib-cmd", "score": 0.2941882834697057}, {"caption": "\\bibsection", "snippet": "\\bibsection", "meta": "natbib-cmd", "score": 0.00038872734530908233}, {"caption": "\\bibsection{}", "snippet": "\\bibsection{$1}", "meta": "natbib-cmd", "score": 0.00038872734530908233}, {"caption": "\\refname", "snippet": "\\refname", "meta": "natbib-cmd", "score": 0.006490238196722249}, {"caption": "\\refname{}", "snippet": "\\refname{$1}", "meta": "natbib-cmd", "score": 0.006490238196722249}, {"caption": "\\citealp{}", "snippet": "\\citealp{$1}", "meta": "natbib-cmd", "score": 0.005275912376595364}, {"caption": "\\citealp[]{}", "snippet": "\\citealp[$1]{$2}", "meta": "natbib-cmd", "score": 0.005275912376595364}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "natbib-cmd", "score": 2.341195220791228}, {"caption": "\\citetalias{}", "snippet": "\\citetalias{$1}", "meta": "natbib-cmd", "score": 0.001419571355756266}, {"caption": "\\bibitem{}", "snippet": "\\bibitem{$1}", "meta": "natbib-cmd", "score": 0.3689547570562042}, {"caption": "\\bibitem[]{}", "snippet": "\\bibitem[$1]{$2}", "meta": "natbib-cmd", "score": 0.3689547570562042}, {"caption": "\\citet{}", "snippet": "\\citet{$1}", "meta": "natbib-cmd", "score": 0.09046048561361801}, {"caption": "\\defcitealias{}{}", "snippet": "\\defcitealias{$1}{$2}", "meta": "natbib-cmd", "score": 0.00042021825647418025}, {"caption": "\\aftergroup", "snippet": "\\aftergroup", "meta": "natbib-cmd", "score": 0.002020423627422133}, {"caption": "\\setcitestyle{}", "snippet": "\\setcitestyle{$1}", "meta": "natbib-cmd", "score": 0.0015840652870152204}, {"caption": "\\citeyearpar{}", "snippet": "\\citeyearpar{$1}", "meta": "natbib-cmd", "score": 0.001877888310324327}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "natbib-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "natbib-cmd", "score": 0.006776001543888959}, {"caption": "\\newblock", "snippet": "\\newblock", "meta": "natbib-cmd", "score": 0.03684301726876973}, {"caption": "\\newblock{}", "snippet": "\\newblock{$1}", "meta": "natbib-cmd", "score": 0.03684301726876973}, {"caption": "\\bibnumfmt", "snippet": "\\bibnumfmt", "meta": "natbib-cmd", "score": 0.000353353600267394}, {"caption": "\\citeyear{}", "snippet": "\\citeyear{$1}", "meta": "natbib-cmd", "score": 0.01091041305836494}, {"caption": "\\citeauthor{}", "snippet": "\\citeauthor{$1}", "meta": "natbib-cmd", "score": 0.01359248786373484}], "url": [{"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "url-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "url-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "url-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "url-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "url-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "url-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "url-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "url-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "url-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "url-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "url-cmd", "score": 3.7048287721105874e-05}], "fontenc": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "fontenc-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "fontenc-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "fontenc-cmd", "score": 0.021170869458413965}], "tikz": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikz-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikz-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikz-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikz-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikz-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikz-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikz-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikz-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikz-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikz-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikz-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikz-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikz-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikz-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikz-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikz-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikz-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikz-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikz-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikz-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikz-cmd", "score": 0.2864294797053033}], "fancyhdr": [{"caption": "\\lhead{}", "snippet": "\\lhead{$1}", "meta": "fancyhdr-cmd", "score": 0.05268978171228714}, {"caption": "\\chaptermark", "snippet": "\\chaptermark", "meta": "fancyhdr-cmd", "score": 0.005924520024686584}, {"caption": "\\chaptermark{}", "snippet": "\\chaptermark{$1}", "meta": "fancyhdr-cmd", "score": 0.005924520024686584}, {"caption": "\\fancypagestyle{}{}", "snippet": "\\fancypagestyle{$1}{$2}", "meta": "fancyhdr-cmd", "score": 0.009430919590937878}, {"caption": "\\footrule", "snippet": "\\footrule", "meta": "fancyhdr-cmd", "score": 0.0010032754348913366}, {"caption": "\\footrule{}", "snippet": "\\footrule{$1}", "meta": "fancyhdr-cmd", "score": 0.0010032754348913366}, {"caption": "\\fancyfoot[]{}", "snippet": "\\fancyfoot[$1]{$2}", "meta": "fancyhdr-cmd", "score": 0.024973618823189894}, {"caption": "\\fancyfoot{}", "snippet": "\\fancyfoot{$1}", "meta": "fancyhdr-cmd", "score": 0.024973618823189894}, {"caption": "\\fancyfootoffset[]{}", "snippet": "\\fancyfootoffset[$1]{$2}", "meta": "fancyhdr-cmd", "score": 0.0015373246231684555}, {"caption": "\\fancyfootoffset{}", "snippet": "\\fancyfootoffset{$1}", "meta": "fancyhdr-cmd", "score": 0.0015373246231684555}, {"caption": "\\footruleskip", "snippet": "\\footruleskip", "meta": "fancyhdr-cmd", "score": 0.000830117957327721}, {"caption": "\\fancyheadoffset[]{}", "snippet": "\\fancyheadoffset[$1]{$2}", "meta": "fancyhdr-cmd", "score": 0.0016786568695309166}, {"caption": "\\fancyheadoffset{}", "snippet": "\\fancyheadoffset{$1}", "meta": "fancyhdr-cmd", "score": 0.0016786568695309166}, {"caption": "\\iffloatpage{}{}", "snippet": "\\iffloatpage{$1}{$2}", "meta": "fancyhdr-cmd", "score": 6.606286310833368e-05}, {"caption": "\\cfoot{}", "snippet": "\\cfoot{$1}", "meta": "fancyhdr-cmd", "score": 0.013411641301057813}, {"caption": "\\subsectionmark", "snippet": "\\subsectionmark", "meta": "fancyhdr-cmd", "score": 3.1153423008593836e-05}, {"caption": "\\footrulewidth", "snippet": "\\footrulewidth", "meta": "fancyhdr-cmd", "score": 0.011424740897486949}, {"caption": "\\fancyhfoffset[]{}", "snippet": "\\fancyhfoffset[$1]{$2}", "meta": "fancyhdr-cmd", "score": 3.741978601121172e-05}, {"caption": "\\rhead{}", "snippet": "\\rhead{$1}", "meta": "fancyhdr-cmd", "score": 0.022782817416731292}, {"caption": "\\fancyplain{}{}", "snippet": "\\fancyplain{$1}{$2}", "meta": "fancyhdr-cmd", "score": 0.007402339896386138}, {"caption": "\\rfoot{}", "snippet": "\\rfoot{$1}", "meta": "fancyhdr-cmd", "score": 0.013393817825547868}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "fancyhdr-cmd", "score": 0.00530510025314411}, {"caption": "\\plainheadrulewidth", "snippet": "\\plainheadrulewidth", "meta": "fancyhdr-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\baselinestretch", "snippet": "\\baselinestretch", "meta": "fancyhdr-cmd", "score": 0.03225350148161425}, {"caption": "\\lfoot{}", "snippet": "\\lfoot{$1}", "meta": "fancyhdr-cmd", "score": 0.00789399846642229}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "fancyhdr-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "fancyhdr-cmd", "score": 0.006776001543888959}, {"caption": "\\fancyhf{}", "snippet": "\\fancyhf{$1}", "meta": "fancyhdr-cmd", "score": 0.02314618933449356}, {"caption": "\\sectionmark", "snippet": "\\sectionmark", "meta": "fancyhdr-cmd", "score": 0.005008938879210868}, {"caption": "\\fancyhead[]{}", "snippet": "\\fancyhead[$1]{$2}", "meta": "fancyhdr-cmd", "score": 0.039101068064744296}, {"caption": "\\fancyhead{}", "snippet": "\\fancyhead{$1}", "meta": "fancyhdr-cmd", "score": 0.039101068064744296}, {"caption": "\\nouppercase{}", "snippet": "\\nouppercase{$1}", "meta": "fancyhdr-cmd", "score": 0.006416387071584083}, {"caption": "\\nouppercase", "snippet": "\\nouppercase", "meta": "fancyhdr-cmd", "score": 0.006416387071584083}, {"caption": "\\headrule", "snippet": "\\headrule", "meta": "fancyhdr-cmd", "score": 0.0008327432627715623}, {"caption": "\\headrule{}", "snippet": "\\headrule{$1}", "meta": "fancyhdr-cmd", "score": 0.0008327432627715623}, {"caption": "\\chead{}", "snippet": "\\chead{$1}", "meta": "fancyhdr-cmd", "score": 0.00755042164734884}, {"caption": "\\headrulewidth", "snippet": "\\headrulewidth", "meta": "fancyhdr-cmd", "score": 0.02268137935335823}], "booktabs": [{"caption": "\\specialrule{}{}{}", "snippet": "\\specialrule{$1}{$2}{$3}", "meta": "booktabs-cmd", "score": 0.004974385202605165}, {"caption": "\\cmidrule", "snippet": "\\cmidrule", "meta": "booktabs-cmd", "score": 0.01894952272365088}, {"caption": "\\cmidrule{}", "snippet": "\\cmidrule{$1}", "meta": "booktabs-cmd", "score": 0.01894952272365088}, {"caption": "\\bottomrule", "snippet": "\\bottomrule", "meta": "booktabs-cmd", "score": 0.04533364657852219}, {"caption": "\\midrule", "snippet": "\\midrule", "meta": "booktabs-cmd", "score": 0.07098077735912875}, {"caption": "\\addlinespace", "snippet": "\\addlinespace", "meta": "booktabs-cmd", "score": 0.005865460617491447}, {"caption": "\\addlinespace[]", "snippet": "\\addlinespace[$1]", "meta": "booktabs-cmd", "score": 0.005865460617491447}, {"caption": "\\toprule", "snippet": "\\toprule", "meta": "booktabs-cmd", "score": 0.059857788139528495}], "amsfonts": [{"caption": "\\frak{}", "snippet": "\\frak{$1}", "meta": "amsfonts-cmd", "score": 0.0017966000518546787}, {"caption": "\\checkmark", "snippet": "\\checkmark", "meta": "amsfonts-cmd", "score": 0.025060530944368123}, {"caption": "\\bold", "snippet": "\\bold", "meta": "amsfonts-cmd", "score": 0.0014358547624941567}, {"caption": "\\bold{}", "snippet": "\\bold{$1}", "meta": "amsfonts-cmd", "score": 0.0014358547624941567}, {"caption": "\\Bbb{}", "snippet": "\\Bbb{$1}", "meta": "amsfonts-cmd", "score": 0.0006671850995492977}, {"caption": "\\Bbb", "snippet": "\\Bbb", "meta": "amsfonts-cmd", "score": 0.0006671850995492977}], "float": [{"caption": "\\listof{}{}", "snippet": "\\listof{$1}{$2}", "meta": "float-cmd", "score": 0.0009837365348002915}, {"caption": "\\floatplacement{}{}", "snippet": "\\floatplacement{$1}{$2}", "meta": "float-cmd", "score": 0.0005815474978918903}, {"caption": "\\restylefloat{}", "snippet": "\\restylefloat{$1}", "meta": "float-cmd", "score": 0.0008866338267686714}, {"caption": "\\floatstyle{}", "snippet": "\\floatstyle{$1}", "meta": "float-cmd", "score": 0.0015470917047414941}, {"caption": "\\floatname{}{}", "snippet": "\\floatname{$1}{$2}", "meta": "float-cmd", "score": 0.0011934321931750752}, {"caption": "\\csname", "snippet": "\\csname", "meta": "float-cmd", "score": 0.008565354665444157}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "float-cmd", "score": 1.2569477427490174}, {"caption": "\\newfloat{}{}{}", "snippet": "\\newfloat{$1}{$2}{$3}", "meta": "float-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat", "snippet": "\\newfloat", "meta": "float-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat{}", "snippet": "\\newfloat{$1}", "meta": "float-cmd", "score": 0.0012745874472536625}], "amsthm": [{"caption": "\\swapnumbers", "snippet": "\\swapnumbers", "meta": "amsthm-cmd", "score": 0.0002908376412221364}, {"caption": "\\qedhere", "snippet": "\\qedhere", "meta": "amsthm-cmd", "score": 0.0001608548097938035}, {"caption": "\\qed", "snippet": "\\qed", "meta": "amsthm-cmd", "score": 0.0014240748825867814}, {"caption": "\\qed{}", "snippet": "\\qed{$1}", "meta": "amsthm-cmd", "score": 0.0014240748825867814}, {"caption": "\\newtheoremstyle{}", "snippet": "\\newtheoremstyle{$1}", "meta": "amsthm-cmd", "score": 0.004259886909451789}, {"caption": "\\newtheoremstyle{}{}{}", "snippet": "\\newtheoremstyle{$1}{$2}{$3}", "meta": "amsthm-cmd", "score": 0.004259886909451789}, {"caption": "\\newtheoremstyle{}{}{}{}", "snippet": "\\newtheoremstyle{$1}{$2}{$3}{$4}", "meta": "amsthm-cmd", "score": 0.004259886909451789}, {"caption": "\\theoremstyle{}", "snippet": "\\theoremstyle{$1}", "meta": "amsthm-cmd", "score": 0.02533412165007986}, {"caption": "\\proofname", "snippet": "\\proofname", "meta": "amsthm-cmd", "score": 0.00021208362094925234}, {"caption": "\\pushQED{}", "snippet": "\\pushQED{$1}", "meta": "amsthm-cmd", "score": 0.00019346981338869148}, {"caption": "\\qedsymbol", "snippet": "\\qedsymbol", "meta": "amsthm-cmd", "score": 0.0022671784428571723}, {"caption": "\\qedsymbol{}", "snippet": "\\qedsymbol{$1}", "meta": "amsthm-cmd", "score": 0.0022671784428571723}, {"caption": "\\popQED", "snippet": "\\popQED", "meta": "amsthm-cmd", "score": 9.673490669434574e-05}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "amsthm-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "amsthm-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "amsthm-cmd", "score": 0.215689795055434}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "amsthm-cmd", "score": 0.0063276692758974925}], "caption": [{"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "caption-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "caption-cmd", "score": 0.02900783226643065}, {"caption": "\\captionof{}{}", "snippet": "\\captionof{$1}{$2}", "meta": "caption-cmd", "score": 0.018348594199161503}, {"caption": "\\string", "snippet": "\\string", "meta": "caption-cmd", "score": 0.001042697111754002}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "caption-cmd", "score": 0.047007158741781095}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "caption-cmd", "score": 0.00530510025314411}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "caption-cmd", "score": 0.0030745841706804776}, {"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "caption-cmd", "score": 0.422097569591803}, {"caption": "\\csname", "snippet": "\\csname", "meta": "caption-cmd", "score": 0.008565354665444157}, {"caption": "\\hspace{}", "snippet": "\\hspace{$1}", "meta": "caption-cmd", "score": 0.3147206476372336}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "caption-cmd", "score": 1.2569477427490174}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "caption-cmd", "score": 1.897791904799601}, {"caption": "\\ContinuedFloat", "snippet": "\\ContinuedFloat", "meta": "caption-cmd", "score": 5.806935368083486e-05}, {"caption": "\\noindent", "snippet": "\\noindent", "meta": "caption-cmd", "score": 0.42355747798114207}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "caption-cmd", "score": 0.00037306820619479756}, {"caption": "\\DeclareCaptionJustification{}{}", "snippet": "\\DeclareCaptionJustification{$1}{$2}", "meta": "caption-cmd", "score": 0.0001872850414971473}, {"caption": "\\DeclareCaptionLabelSeparator{}{}", "snippet": "\\DeclareCaptionLabelSeparator{$1}{$2}", "meta": "caption-cmd", "score": 0.0003890810058478364}, {"caption": "\\DeclareCaptionFormat{}{}", "snippet": "\\DeclareCaptionFormat{$1}{$2}", "meta": "caption-cmd", "score": 0.0004717618449370015}, {"caption": "\\DeclareCaptionFont{}{}", "snippet": "\\DeclareCaptionFont{$1}{$2}", "meta": "caption-cmd", "score": 5.0133404990680195e-05}, {"caption": "\\DeclareCaptionSubType[]{}", "snippet": "\\DeclareCaptionSubType[$1]{$2}", "meta": "caption-cmd", "score": 0.0001872850414971473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "caption-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "caption-cmd", "score": 0.021170869458413965}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "caption-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "caption-cmd", "score": 0.02900783226643065}, {"caption": "\\string", "snippet": "\\string", "meta": "caption-cmd", "score": 0.001042697111754002}, {"caption": "\\DeclareCaptionType{}[][]", "snippet": "\\DeclareCaptionType{$1}[$2][$3]", "meta": "caption-cmd", "score": 0.00015256647321237863}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "caption-cmd", "score": 0.00530510025314411}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "caption-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "caption-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "caption-cmd", "score": 0.021473212893597875}], "ifthen": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "ifthen-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "ifthen-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "ifthen-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "ifthen-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "ifthen-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "ifthen-cmd", "score": 0.0018957469739775527}], "setspace": [{"caption": "\\setstretch{}", "snippet": "\\setstretch{$1}", "meta": "setspace-cmd", "score": 0.019634763572332112}, {"caption": "\\onehalfspacing", "snippet": "\\onehalfspacing", "meta": "setspace-cmd", "score": 0.010655415521079565}, {"caption": "\\singlespacing", "snippet": "\\singlespacing", "meta": "setspace-cmd", "score": 0.008351544612280968}, {"caption": "\\doublespacing", "snippet": "\\doublespacing", "meta": "setspace-cmd", "score": 0.007835428951987135}, {"caption": "\\baselinestretch", "snippet": "\\baselinestretch", "meta": "setspace-cmd", "score": 0.03225350148161425}], "multirow": [{"caption": "\\multirow{}{}{}", "snippet": "\\multirow{$1}{$2}{$3}", "meta": "multirow-cmd", "score": 0.07525389638751734}, {"caption": "\\multirow{}[]{}{}", "snippet": "\\multirow{$1}[$2]{$3}{$4}", "meta": "multirow-cmd", "score": 0.07525389638751734}], "array": [{"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "array-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "array-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "array-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "array-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "array-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "array-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "array-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "array-cmd", "score": 0.018615449342361392}], "titlesec": [{"caption": "\\titleclass{}{}[]", "snippet": "\\titleclass{$1}{$2}[$3]", "meta": "titlesec-cmd", "score": 0.00028979763314974667}, {"caption": "\\titlelabel{}", "snippet": "\\titlelabel{$1}", "meta": "titlesec-cmd", "score": 6.40387839367932e-06}, {"caption": "\\thetitle", "snippet": "\\thetitle", "meta": "titlesec-cmd", "score": 0.0015531478302713473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "titlesec-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "titlesec-cmd", "score": 0.021170869458413965}, {"caption": "\\titleformat{}{}{}{}{}[]", "snippet": "\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]", "meta": "titlesec-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}[]{}{}{}{}", "snippet": "\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "titlesec-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}{}", "snippet": "\\titleformat{$1}{$2}", "meta": "titlesec-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}{}{}{}{}", "snippet": "\\titleformat{$1}{$2}{$3}{$4}{$5}", "meta": "titlesec-cmd", "score": 0.03475519439740096}, {"caption": "\\titlespacing{}{}{}{}", "snippet": "\\titlespacing{$1}{$2}{$3}{$4}", "meta": "titlesec-cmd", "score": 0.023062744385192156}, {"caption": "\\markboth{}{}", "snippet": "\\markboth{$1}{$2}", "meta": "titlesec-cmd", "score": 0.038323601301945065}, {"caption": "\\markboth{}", "snippet": "\\markboth{$1}", "meta": "titlesec-cmd", "score": 0.038323601301945065}, {"caption": "\\markright{}", "snippet": "\\markright{$1}", "meta": "titlesec-cmd", "score": 0.007138622674767024}, {"caption": "\\markright{}{}", "snippet": "\\markright{$1}{$2}", "meta": "titlesec-cmd", "score": 0.007138622674767024}, {"caption": "\\filleft", "snippet": "\\filleft", "meta": "titlesec-cmd", "score": 7.959989906732799e-05}, {"caption": "\\filcenter", "snippet": "\\filcenter", "meta": "titlesec-cmd", "score": 0.0004835660211260246}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "titlesec-cmd", "score": 0.2253056071787701}, {"caption": "\\cleardoublepage", "snippet": "\\cleardoublepage", "meta": "titlesec-cmd", "score": 0.044016804142963585}, {"caption": "\\csname", "snippet": "\\csname", "meta": "titlesec-cmd", "score": 0.008565354665444157}, {"caption": "\\chaptertitlename", "snippet": "\\chaptertitlename", "meta": "titlesec-cmd", "score": 0.0016985007766926272}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "titlesec-cmd", "score": 0.3277033727934986}, {"caption": "\\filright", "snippet": "\\filright", "meta": "titlesec-cmd", "score": 7.959989906732799e-05}, {"caption": "\\titlerule", "snippet": "\\titlerule", "meta": "titlesec-cmd", "score": 0.019273712561461216}, {"caption": "\\titlerule[]{}", "snippet": "\\titlerule[$1]{$2}", "meta": "titlesec-cmd", "score": 0.019273712561461216}], "multicol": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "multicol-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "multicol-cmd", "score": 0.021170869458413965}, {"caption": "\\raggedcolumns", "snippet": "\\raggedcolumns", "meta": "multicol-cmd", "score": 0.00027461965178228156}, {"caption": "\\columnbreak", "snippet": "\\columnbreak", "meta": "multicol-cmd", "score": 0.002609610141555795}, {"caption": "\\columnseprulecolor{}", "snippet": "\\columnseprulecolor{$1}", "meta": "multicol-cmd", "score": 1.3314892207625771e-05}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "multicol-cmd", "score": 0.1789117552185788}], "listings": [{"caption": "\\vskip", "snippet": "\\vskip", "meta": "listings-cmd", "score": 0.05143052892347224}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "listings-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "listings-cmd", "score": 0.021170869458413965}, {"caption": "\\do", "snippet": "\\do", "meta": "listings-cmd", "score": 0.009278344180101056}, {"caption": "\\thelstlisting", "snippet": "\\thelstlisting", "meta": "listings-cmd", "score": 0.00012774128088872144}, {"caption": "\\lstinputlisting[]{}", "snippet": "\\lstinputlisting[$1]{$2}", "meta": "listings-cmd", "score": 0.011660477607086044}, {"caption": "\\lstinputlisting{}", "snippet": "\\lstinputlisting{$1}", "meta": "listings-cmd", "score": 0.011660477607086044}, {"caption": "\\space", "snippet": "\\space", "meta": "listings-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "listings-cmd", "score": 0.008565354665444157}, {"caption": "\\lstinline", "snippet": "\\lstinline", "meta": "listings-cmd", "score": 0.005972262850694285}, {"caption": "\\lstinline{}", "snippet": "\\lstinline{$1}", "meta": "listings-cmd", "score": 0.005972262850694285}, {"caption": "\\lstlistoflistings", "snippet": "\\lstlistoflistings", "meta": "listings-cmd", "score": 0.005279080363360602}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "listings-cmd", "score": 0.00037306820619479756}], "blindtext": [{"caption": "\\glqq", "snippet": "\\glqq", "meta": "blindtext-cmd", "score": 0.0039133256714254504}, {"caption": "\\glqq{}", "snippet": "\\glqq{$1}", "meta": "blindtext-cmd", "score": 0.0039133256714254504}, {"caption": "\\blindtext", "snippet": "\\blindtext", "meta": "blindtext-cmd", "score": 0.05782040856823667}, {"caption": "\\blindtext[]", "snippet": "\\blindtext[$1]", "meta": "blindtext-cmd", "score": 0.05782040856823667}, {"caption": "\\Blindtext", "snippet": "\\Blindtext", "meta": "blindtext-cmd", "score": 0.006384906903938044}, {"caption": "\\grqq", "snippet": "\\grqq", "meta": "blindtext-cmd", "score": 0.006659522189248266}, {"caption": "\\grqq{}", "snippet": "\\grqq{$1}", "meta": "blindtext-cmd", "score": 0.006659522189248266}, {"caption": "\\blinddocument", "snippet": "\\blinddocument", "meta": "blindtext-cmd", "score": 0.00011480988129172825}, {"caption": "\\xspace", "snippet": "\\xspace", "meta": "blindtext-cmd", "score": 0.07560370351316588}], "enumitem": [{"caption": "\\newlist{}{}{}", "snippet": "\\newlist{$1}{$2}{$3}", "meta": "enumitem-cmd", "score": 0.0007266225924074459}, {"caption": "\\setlist[]{}", "snippet": "\\setlist[$1]{$2}", "meta": "enumitem-cmd", "score": 0.010895384475728338}, {"caption": "\\setlist{}", "snippet": "\\setlist{$1}", "meta": "enumitem-cmd", "score": 0.010895384475728338}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "enumitem-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "enumitem-cmd", "score": 0.021170869458413965}, {"caption": "\\setlistdepth{}", "snippet": "\\setlistdepth{$1}", "meta": "enumitem-cmd", "score": 0.0001113322912630871}, {"caption": "\\setenumerate[]{}", "snippet": "\\setenumerate[$1]{$2}", "meta": "enumitem-cmd", "score": 7.437178301071255e-05}, {"caption": "\\setenumerate{}", "snippet": "\\setenumerate{$1}", "meta": "enumitem-cmd", "score": 7.437178301071255e-05}, {"caption": "\\renewlist{}{}{}", "snippet": "\\renewlist{$1}{$2}{$3}", "meta": "enumitem-cmd", "score": 0.0001113322912630871}, {"caption": "\\descriptionlabel{}", "snippet": "\\descriptionlabel{$1}", "meta": "enumitem-cmd", "score": 7.678089052626698e-06}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "enumitem-cmd", "score": 0.00530510025314411}, {"caption": "\\setitemize[]{}", "snippet": "\\setitemize[$1]{$2}", "meta": "enumitem-cmd", "score": 0.0019580640711971786}, {"caption": "\\csname", "snippet": "\\csname", "meta": "enumitem-cmd", "score": 0.008565354665444157}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "enumitem-cmd", "score": 0.01590723355124104}, {"caption": "\\makelabel", "snippet": "\\makelabel", "meta": "enumitem-cmd", "score": 5.739925426740175e-05}, {"caption": "\\makelabel{}", "snippet": "\\makelabel{$1}", "meta": "enumitem-cmd", "score": 5.739925426740175e-05}, {"caption": "\\makelabel[]{}", "snippet": "\\makelabel[$1]{$2}", "meta": "enumitem-cmd", "score": 5.739925426740175e-05}], "times": [{"caption": "\\rmdefault", "snippet": "\\rmdefault", "meta": "times-cmd", "score": 0.0012870877747432935}, {"caption": "\\sfdefault", "snippet": "\\sfdefault", "meta": "times-cmd", "score": 0.008427383388519996}, {"caption": "\\sfdefault{}", "snippet": "\\sfdefault{$1}", "meta": "times-cmd", "score": 0.008427383388519996}, {"caption": "\\ttdefault", "snippet": "\\ttdefault", "meta": "times-cmd", "score": 0.0011733254149332488}, {"caption": "\\ttdefault{}", "snippet": "\\ttdefault{$1}", "meta": "times-cmd", "score": 0.0011733254149332488}], "subcaption": [{"caption": "\\subref{}", "snippet": "\\subref{$1}", "meta": "subcaption-cmd", "score": 0.007192033516871399}, {"caption": "\\subcaptionbox{}{}", "snippet": "\\subcaptionbox{$1}{$2}", "meta": "subcaption-cmd", "score": 0.0008634329663023698}, {"caption": "\\newsubfloat{}", "snippet": "\\newsubfloat{$1}", "meta": "subcaption-cmd", "score": 0.000615805121082521}, {"caption": "\\subcaption{}", "snippet": "\\subcaption{$1}", "meta": "subcaption-cmd", "score": 0.006820005741581297}, {"caption": "\\subcaption[]{}", "snippet": "\\subcaption[$1]{$2}", "meta": "subcaption-cmd", "score": 0.006820005741581297}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "subcaption-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "subcaption-cmd", "score": 0.02900783226643065}, {"caption": "\\captionof{}{}", "snippet": "\\captionof{$1}{$2}", "meta": "subcaption-cmd", "score": 0.018348594199161503}, {"caption": "\\string", "snippet": "\\string", "meta": "subcaption-cmd", "score": 0.001042697111754002}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "subcaption-cmd", "score": 0.047007158741781095}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "subcaption-cmd", "score": 0.00530510025314411}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "subcaption-cmd", "score": 0.0030745841706804776}, {"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "subcaption-cmd", "score": 0.422097569591803}, {"caption": "\\csname", "snippet": "\\csname", "meta": "subcaption-cmd", "score": 0.008565354665444157}, {"caption": "\\hspace{}", "snippet": "\\hspace{$1}", "meta": "subcaption-cmd", "score": 0.3147206476372336}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "subcaption-cmd", "score": 1.2569477427490174}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "subcaption-cmd", "score": 1.897791904799601}, {"caption": "\\ContinuedFloat", "snippet": "\\ContinuedFloat", "meta": "subcaption-cmd", "score": 5.806935368083486e-05}, {"caption": "\\noindent", "snippet": "\\noindent", "meta": "subcaption-cmd", "score": 0.42355747798114207}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "subcaption-cmd", "score": 0.00037306820619479756}, {"caption": "\\DeclareCaptionJustification{}{}", "snippet": "\\DeclareCaptionJustification{$1}{$2}", "meta": "subcaption-cmd", "score": 0.0001872850414971473}, {"caption": "\\DeclareCaptionLabelSeparator{}{}", "snippet": "\\DeclareCaptionLabelSeparator{$1}{$2}", "meta": "subcaption-cmd", "score": 0.0003890810058478364}, {"caption": "\\DeclareCaptionFormat{}{}", "snippet": "\\DeclareCaptionFormat{$1}{$2}", "meta": "subcaption-cmd", "score": 0.0004717618449370015}, {"caption": "\\DeclareCaptionFont{}{}", "snippet": "\\DeclareCaptionFont{$1}{$2}", "meta": "subcaption-cmd", "score": 5.0133404990680195e-05}, {"caption": "\\DeclareCaptionSubType[]{}", "snippet": "\\DeclareCaptionSubType[$1]{$2}", "meta": "subcaption-cmd", "score": 0.0001872850414971473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "subcaption-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "subcaption-cmd", "score": 0.021170869458413965}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "subcaption-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "subcaption-cmd", "score": 0.02900783226643065}, {"caption": "\\string", "snippet": "\\string", "meta": "subcaption-cmd", "score": 0.001042697111754002}, {"caption": "\\DeclareCaptionType{}[][]", "snippet": "\\DeclareCaptionType{$1}[$2][$3]", "meta": "subcaption-cmd", "score": 0.00015256647321237863}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "subcaption-cmd", "score": 0.00530510025314411}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "subcaption-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "subcaption-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "subcaption-cmd", "score": 0.021473212893597875}], "bm": [{"caption": "\\bm{}", "snippet": "\\bm{$1}", "meta": "bm-cmd", "score": 0.14733018077819282}, {"caption": "\\bm", "snippet": "\\bm", "meta": "bm-cmd", "score": 0.14733018077819282}], "fontspec": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "fontspec-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "fontspec-cmd", "score": 0.2864294797053033}], "subfigure": [{"caption": "\\subref{}", "snippet": "\\subref{$1}", "meta": "subfigure-cmd", "score": 0.007192033516871399}, {"caption": "\\subfigure[]{}", "snippet": "\\subfigure[$1]{$2}", "meta": "subfigure-cmd", "score": 0.037856842641104005}], "calc": [{"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "calc-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "calc-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "calc-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "calc-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "calc-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "calc-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "calc-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "calc-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "calc-cmd", "score": 0.028955796305270766}], "tabularx": [{"caption": "\\let", "snippet": "\\let", "meta": "tabularx-cmd", "score": 0.03789745970461662}, {"caption": "\\write", "snippet": "\\write", "meta": "tabularx-cmd", "score": 0.0008038857295393196}, {"caption": "\\tabularxcolumn[]{}", "snippet": "\\tabularxcolumn[$1]{$2}", "meta": "tabularx-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularxcolumn", "snippet": "\\tabularxcolumn", "meta": "tabularx-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularx{}{}", "snippet": "\\tabularx{$1}{$2}", "meta": "tabularx-cmd", "score": 0.0005861357565780464}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "tabularx-cmd", "score": 0.014532521139459619}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "tabularx-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "tabularx-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "tabularx-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "tabularx-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "tabularx-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tabularx-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "tabularx-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "tabularx-cmd", "score": 0.018615449342361392}], "algorithm": [{"caption": "\\listalgorithmname", "snippet": "\\listalgorithmname", "meta": "algorithm-cmd", "score": 0.00022490402516652368}, {"caption": "\\listofalgorithms", "snippet": "\\listofalgorithms", "meta": "algorithm-cmd", "score": 0.0012576983422794912}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "algorithm-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "algorithm-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "algorithm-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "algorithm-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "algorithm-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "algorithm-cmd", "score": 0.0018957469739775527}, {"caption": "\\listof{}{}", "snippet": "\\listof{$1}{$2}", "meta": "algorithm-cmd", "score": 0.0009837365348002915}, {"caption": "\\floatplacement{}{}", "snippet": "\\floatplacement{$1}{$2}", "meta": "algorithm-cmd", "score": 0.0005815474978918903}, {"caption": "\\restylefloat{}", "snippet": "\\restylefloat{$1}", "meta": "algorithm-cmd", "score": 0.0008866338267686714}, {"caption": "\\floatstyle{}", "snippet": "\\floatstyle{$1}", "meta": "algorithm-cmd", "score": 0.0015470917047414941}, {"caption": "\\floatname{}{}", "snippet": "\\floatname{$1}{$2}", "meta": "algorithm-cmd", "score": 0.0011934321931750752}, {"caption": "\\csname", "snippet": "\\csname", "meta": "algorithm-cmd", "score": 0.008565354665444157}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "algorithm-cmd", "score": 1.2569477427490174}, {"caption": "\\newfloat{}{}{}", "snippet": "\\newfloat{$1}{$2}{$3}", "meta": "algorithm-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat", "snippet": "\\newfloat", "meta": "algorithm-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat{}", "snippet": "\\newfloat{$1}", "meta": "algorithm-cmd", "score": 0.0012745874472536625}], "biblatex": [{"caption": "\\textcite{}", "snippet": "\\textcite{$1}", "meta": "biblatex-cmd", "score": 0.0071363824748767206}, {"caption": "\\iffieldundef{}{}{}", "snippet": "\\iffieldundef{$1}{$2}{$3}", "meta": "biblatex-cmd", "score": 4.841482597532878e-05}, {"caption": "\\list{}{}", "snippet": "\\list{$1}{$2}", "meta": "biblatex-cmd", "score": 0.00046570666700199663}, {"caption": "\\list{}", "snippet": "\\list{$1}", "meta": "biblatex-cmd", "score": 0.00046570666700199663}, {"caption": "\\list", "snippet": "\\list", "meta": "biblatex-cmd", "score": 0.00046570666700199663}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "biblatex-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "biblatex-cmd", "score": 0.021170869458413965}, {"caption": "\\printbibliography", "snippet": "\\printbibliography", "meta": "biblatex-cmd", "score": 0.028923378512954446}, {"caption": "\\printbibliography[]", "snippet": "\\printbibliography[$1]", "meta": "biblatex-cmd", "score": 0.028923378512954446}, {"caption": "\\keyword{}", "snippet": "\\keyword{$1}", "meta": "biblatex-cmd", "score": 0.0056978719547823445}, {"caption": "\\nocite{}", "snippet": "\\nocite{$1}", "meta": "biblatex-cmd", "score": 0.04990693820960752}, {"caption": "\\do", "snippet": "\\do", "meta": "biblatex-cmd", "score": 0.009278344180101056}, {"caption": "\\mkbibquote{}", "snippet": "\\mkbibquote{$1}", "meta": "biblatex-cmd", "score": 4.841482597532878e-05}, {"caption": "\\addabbrvspace", "snippet": "\\addabbrvspace", "meta": "biblatex-cmd", "score": 4.841482597532878e-05}, {"caption": "\\AtEveryBibitem{}", "snippet": "\\AtEveryBibitem{$1}", "meta": "biblatex-cmd", "score": 0.0006862523808353773}, {"caption": "\\mkbibemph{}", "snippet": "\\mkbibemph{$1}", "meta": "biblatex-cmd", "score": 4.841482597532878e-05}, {"caption": "\\DeclareFieldFormat{}{}", "snippet": "\\DeclareFieldFormat{$1}{$2}", "meta": "biblatex-cmd", "score": 0.00028207109055618685}, {"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "biblatex-cmd", "score": 0.2659628337907604}, {"caption": "\\enquote{}", "snippet": "\\enquote{$1}", "meta": "biblatex-cmd", "score": 0.0077432730806830915}, {"caption": "\\bibopenbracket", "snippet": "\\bibopenbracket", "meta": "biblatex-cmd", "score": 0.0005125772067631753}, {"caption": "\\newbibmacro{}[]{}", "snippet": "\\newbibmacro{$1}[$2]{$3}", "meta": "biblatex-cmd", "score": 4.841482597532878e-05}, {"caption": "\\addbibresource{}", "snippet": "\\addbibresource{$1}", "meta": "biblatex-cmd", "score": 0.033545778388159704}, {"caption": "\\defbibheading{}{}", "snippet": "\\defbibheading{$1}{$2}", "meta": "biblatex-cmd", "score": 0.00013423526504458629}, {"caption": "\\DeclareNameAlias{}{}", "snippet": "\\DeclareNameAlias{$1}{$2}", "meta": "biblatex-cmd", "score": 0.0003596306478652252}, {"caption": "\\bibcloseparen", "snippet": "\\bibcloseparen", "meta": "biblatex-cmd", "score": 0.0005125772067631753}, {"caption": "\\renewbibmacro{}{}", "snippet": "\\renewbibmacro{$1}{$2}", "meta": "biblatex-cmd", "score": 9.70299207241043e-05}, {"caption": "\\bibclosebracket", "snippet": "\\bibclosebracket", "meta": "biblatex-cmd", "score": 0.0005125772067631753}, {"caption": "\\item", "snippet": "\\item", "meta": "biblatex-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "biblatex-cmd", "score": 3.800886892251021}, {"caption": "\\parentext", "snippet": "\\parentext", "meta": "biblatex-cmd", "score": 0.0005125772067631753}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "biblatex-cmd", "score": 2.341195220791228}, {"caption": "\\addspace", "snippet": "\\addspace", "meta": "biblatex-cmd", "score": 0.0002657609533376918}, {"caption": "\\ifentrytype{}{}{}", "snippet": "\\ifentrytype{$1}{$2}{$3}", "meta": "biblatex-cmd", "score": 8.342875497183237e-05}, {"caption": "\\addslash", "snippet": "\\addslash", "meta": "biblatex-cmd", "score": 0.0002657609533376918}, {"caption": "\\DefineBibliographyStrings{}{}", "snippet": "\\DefineBibliographyStrings{$1}{$2}", "meta": "biblatex-cmd", "score": 0.001537977148659816}, {"caption": "\\section{}", "snippet": "\\section{$1}", "meta": "biblatex-cmd", "score": 3.0952612541683835}, {"caption": "\\newblockpunct", "snippet": "\\newblockpunct", "meta": "biblatex-cmd", "score": 0.0001328804766688459}, {"caption": "\\defbibfilter{}{}", "snippet": "\\defbibfilter{$1}{$2}", "meta": "biblatex-cmd", "score": 0.0005203319717980072}, {"caption": "\\parencite{}", "snippet": "\\parencite{$1}", "meta": "biblatex-cmd", "score": 0.0447747090014577}, {"caption": "\\parencite[]{}", "snippet": "\\parencite[$1]{$2}", "meta": "biblatex-cmd", "score": 0.0447747090014577}, {"caption": "\\midsentence", "snippet": "\\midsentence", "meta": "biblatex-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\nolinkurl{}", "snippet": "\\nolinkurl{$1}", "meta": "biblatex-cmd", "score": 0.0004995635515943437}, {"caption": "\\DeclareSourcemap{}", "snippet": "\\DeclareSourcemap{$1}", "meta": "biblatex-cmd", "score": 0.0005203319717980072}, {"caption": "\\AtBeginBibliography{}", "snippet": "\\AtBeginBibliography{$1}", "meta": "biblatex-cmd", "score": 0.0004668773504581073}, {"caption": "\\AtEveryCite{}", "snippet": "\\AtEveryCite{$1}", "meta": "biblatex-cmd", "score": 0.0005125772067631753}, {"caption": "\\DeclareLanguageMapping{}{}", "snippet": "\\DeclareLanguageMapping{$1}{$2}", "meta": "biblatex-cmd", "score": 0.000703956971675325}, {"caption": "\\addtocategory{}{}", "snippet": "\\addtocategory{$1}{$2}", "meta": "biblatex-cmd", "score": 0.008238589553468446}, {"caption": "\\DeclareBibliographyCategory{}", "snippet": "\\DeclareBibliographyCategory{$1}", "meta": "biblatex-cmd", "score": 0.0010298236941835557}, {"caption": "\\break", "snippet": "\\break", "meta": "biblatex-cmd", "score": 0.016352452390960115}, {"caption": "\\break{}", "snippet": "\\break{$1}", "meta": "biblatex-cmd", "score": 0.016352452390960115}, {"caption": "\\break{}{}", "snippet": "\\break{$1}{$2}", "meta": "biblatex-cmd", "score": 0.016352452390960115}, {"caption": "\\bibopenparen", "snippet": "\\bibopenparen", "meta": "biblatex-cmd", "score": 0.0005125772067631753}, {"caption": "\\csname", "snippet": "\\csname", "meta": "biblatex-cmd", "score": 0.008565354665444157}, {"caption": "\\name{}{}", "snippet": "\\name{$1}{$2}", "meta": "biblatex-cmd", "score": 0.1236289144754329}, {"caption": "\\name", "snippet": "\\name", "meta": "biblatex-cmd", "score": 0.1236289144754329}, {"caption": "\\name{}", "snippet": "\\name{$1}", "meta": "biblatex-cmd", "score": 0.1236289144754329}, {"caption": "\\ExecuteBibliographyOptions{}", "snippet": "\\ExecuteBibliographyOptions{$1}", "meta": "biblatex-cmd", "score": 4.841482597532878e-05}, {"caption": "\\usebibmacro{}{}", "snippet": "\\usebibmacro{$1}{$2}", "meta": "biblatex-cmd", "score": 9.682965195065755e-05}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "biblatex-cmd", "score": 0.00037306820619479756}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "biblatex-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "biblatex-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "biblatex-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "biblatex-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "biblatex-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "biblatex-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "biblatex-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "biblatex-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "biblatex-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "biblatex-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "biblatex-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\do", "snippet": "\\do", "meta": "biblatex-cmd", "score": 0.009278344180101056}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "biblatex-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "biblatex-cmd", "score": 0.021170869458413965}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "biblatex-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "biblatex-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "biblatex-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "biblatex-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "biblatex-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "biblatex-cmd", "score": 0.0018957469739775527}, {"caption": "\\csname", "snippet": "\\csname", "meta": "biblatex-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "biblatex-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "biblatex-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "biblatex-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "biblatex-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "biblatex-cmd", "score": 0.021170869458413965}, {"caption": "\\empty", "snippet": "\\empty", "meta": "biblatex-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "biblatex-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "biblatex-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "biblatex-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "biblatex-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "biblatex-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "biblatex-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "biblatex-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "biblatex-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "biblatex-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "biblatex-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "biblatex-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "biblatex-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "biblatex-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "biblatex-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "biblatex-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "biblatex-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "biblatex-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "biblatex-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "biblatex-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "biblatex-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "biblatex-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "biblatex-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "biblatex-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "biblatex-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "biblatex-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "biblatex-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "biblatex-cmd", "score": 0.008565354665444157}], "microtype": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "microtype-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "microtype-cmd", "score": 0.021170869458413965}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "microtype-cmd", "score": 0.00530510025314411}, {"caption": "\\lsstyle", "snippet": "\\lsstyle", "meta": "microtype-cmd", "score": 0.0023367519914345774}, {"caption": "\\space", "snippet": "\\space", "meta": "microtype-cmd", "score": 0.023010789853665694}, {"caption": "\\DisableLigatures[]{}", "snippet": "\\DisableLigatures[$1]{$2}", "meta": "microtype-cmd", "score": 0.0009805246614299932}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "microtype-cmd", "score": 0.00037306820619479756}], "etoolbox": [{"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "etoolbox-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "etoolbox-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "etoolbox-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "etoolbox-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "etoolbox-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "etoolbox-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "etoolbox-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "etoolbox-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "etoolbox-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "etoolbox-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "etoolbox-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "etoolbox-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "etoolbox-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "etoolbox-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "etoolbox-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "etoolbox-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "etoolbox-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "etoolbox-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "etoolbox-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "etoolbox-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "etoolbox-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "etoolbox-cmd", "score": 0.008565354665444157}], "longtable": [{"caption": "\\endhead", "snippet": "\\endhead", "meta": "longtable-cmd", "score": 0.0023853501147448834}, {"caption": "\\endfoot", "snippet": "\\endfoot", "meta": "longtable-cmd", "score": 0.00044045261916551967}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "longtable-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "longtable-cmd", "score": 0.021170869458413965}, {"caption": "\\nopagebreak", "snippet": "\\nopagebreak", "meta": "longtable-cmd", "score": 9.952664522415981e-05}, {"caption": "\\endfirsthead", "snippet": "\\endfirsthead", "meta": "longtable-cmd", "score": 0.0016148498709822416}, {"caption": "\\endlastfoot", "snippet": "\\endlastfoot", "meta": "longtable-cmd", "score": 0.00044045261916551967}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "longtable-cmd", "score": 0.3277033727934986}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "longtable-cmd", "score": 0.0029238994233674776}, {"caption": "\\pagebreak", "snippet": "\\pagebreak", "meta": "longtable-cmd", "score": 0.0313525090421608}], "mathtools": [{"caption": "\\xleftrightarrow[][]{}", "snippet": "\\xleftrightarrow[$1][$2]{$3}", "meta": "mathtools-cmd", "score": 4.015559489911509e-05}, {"caption": "\\vcentcolon", "snippet": "\\vcentcolon", "meta": "mathtools-cmd", "score": 0.00021361943526711615}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "mathtools-cmd", "score": 0.0016148076375871775}, {"caption": "\\coloneqq", "snippet": "\\coloneqq", "meta": "mathtools-cmd", "score": 0.0014407293323958122}, {"caption": "\\mathclap{}", "snippet": "\\mathclap{$1}", "meta": "mathtools-cmd", "score": 7.84378567451772e-05}, {"caption": "\\adjustlimits", "snippet": "\\adjustlimits", "meta": "mathtools-cmd", "score": 0.0005307066890271085}, {"caption": "\\MoveEqLeft", "snippet": "\\MoveEqLeft", "meta": "mathtools-cmd", "score": 5.343949980628182e-05}, {"caption": "\\mathrlap{}", "snippet": "\\mathrlap{$1}", "meta": "mathtools-cmd", "score": 0.0003112817211637952}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "mathtools-cmd", "score": 0.051980653969641216}, {"caption": "\\xhookrightarrow{}", "snippet": "\\xhookrightarrow{$1}", "meta": "mathtools-cmd", "score": 5.444260823474129e-05}, {"caption": "\\DeclarePairedDelimiter{}{}{}", "snippet": "\\DeclarePairedDelimiter{$1}{$2}{$3}", "meta": "mathtools-cmd", "score": 0.0033916678416372487}, {"caption": "\\DeclarePairedDelimiter", "snippet": "\\DeclarePairedDelimiter", "meta": "mathtools-cmd", "score": 0.0033916678416372487}, {"caption": "\\prescript{}{}{}", "snippet": "\\prescript{$1}{$2}{$3}", "meta": "mathtools-cmd", "score": 8.833369785705982e-06}, {"caption": "\\underbrace{}", "snippet": "\\underbrace{$1}", "meta": "mathtools-cmd", "score": 0.010373780436850907}, {"caption": "\\mathllap{}", "snippet": "\\mathllap{$1}", "meta": "mathtools-cmd", "score": 3.140504277052775e-05}, {"caption": "\\overbrace{}", "snippet": "\\overbrace{$1}", "meta": "mathtools-cmd", "score": 0.0006045704778718376}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "mathtools-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "mathtools-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "mathtools-cmd", "score": 0.18137737738638837}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "mathtools-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "mathtools-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "mathtools-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "mathtools-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mathtools-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mathtools-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "mathtools-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "mathtools-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "mathtools-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "mathtools-cmd", "score": 0.028955796305270766}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mathtools-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mathtools-cmd", "score": 0.021170869458413965}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "mathtools-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "mathtools-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "mathtools-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "mathtools-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "mathtools-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "mathtools-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "mathtools-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "mathtools-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "mathtools-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "mathtools-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "mathtools-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "mathtools-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "mathtools-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "mathtools-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "mathtools-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "mathtools-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "mathtools-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "mathtools-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "mathtools-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "mathtools-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "mathtools-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "mathtools-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "mathtools-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "mathtools-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "mathtools-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "mathtools-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "mathtools-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "mathtools-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "mathtools-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "mathtools-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "mathtools-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "mathtools-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "mathtools-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "mathtools-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "mathtools-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "mathtools-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "mathtools-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "mathtools-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "mathtools-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "mathtools-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "mathtools-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "mathtools-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "mathtools-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "mathtools-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "mathtools-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "mathtools-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "mathtools-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "mathtools-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "mathtools-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "mathtools-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "mathtools-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "mathtools-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "mathtools-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "mathtools-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "mathtools-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "mathtools-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "mathtools-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "mathtools-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "mathtools-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "mathtools-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "mathtools-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "mathtools-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "mathtools-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "mathtools-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "mathtools-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "mathtools-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "mathtools-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "mathtools-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "mathtools-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "mathtools-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "mathtools-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "mathtools-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "mathtools-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "mathtools-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "mathtools-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "mathtools-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "mathtools-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "mathtools-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "mathtools-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "mathtools-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "mathtools-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "mathtools-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "mathtools-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "mathtools-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "mathtools-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "mathtools-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "mathtools-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "mathtools-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "mathtools-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "mathtools-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "mathtools-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "mathtools-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "mathtools-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "mathtools-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "mathtools-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "mathtools-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "mathtools-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "mathtools-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "mathtools-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "mathtools-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "mathtools-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "mathtools-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "mathtools-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "mathtools-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "mathtools-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "mathtools-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "mathtools-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "mathtools-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "mathtools-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "mathtools-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "mathtools-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "mathtools-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "mathtools-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "mathtools-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "mathtools-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "mathtools-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "mathtools-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "mathtools-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "mathtools-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "mathtools-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "mathtools-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "mathtools-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "mathtools-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "mathtools-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "mathtools-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "mathtools-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "mathtools-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "mathtools-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "mathtools-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "mathtools-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "mathtools-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "mathtools-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "mathtools-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "mathtools-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "mathtools-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "mathtools-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "mathtools-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "mathtools-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "mathtools-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "mathtools-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "mathtools-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "mathtools-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "mathtools-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "mathtools-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "mathtools-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "mathtools-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mathtools-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "mathtools-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "mathtools-cmd", "score": 0.0063276692758974925}], "verbatim": [{"caption": "\\endverbatim", "snippet": "\\endverbatim", "meta": "verbatim-cmd", "score": 0.0022216421267780076}, {"caption": "\\verbatim", "snippet": "\\verbatim", "meta": "verbatim-cmd", "score": 0.0072203369120285256}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "verbatim-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "verbatim-cmd", "score": 0.021170869458413965}, {"caption": "\\par", "snippet": "\\par", "meta": "verbatim-cmd", "score": 0.413853376001159}, {"caption": "\\verbatiminput{}", "snippet": "\\verbatiminput{$1}", "meta": "verbatim-cmd", "score": 0.0024547099784948665}, {"caption": "\\verbatiminput", "snippet": "\\verbatiminput", "meta": "verbatim-cmd", "score": 0.0024547099784948665}], "wrapfig": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "wrapfig-cmd", "score": 0.00530510025314411}, {"caption": "\\par", "snippet": "\\par", "meta": "wrapfig-cmd", "score": 0.413853376001159}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "wrapfig-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "wrapfig-cmd", "score": 0.021170869458413965}, {"caption": "\\wrapfigure{}{}", "snippet": "\\wrapfigure{$1}{$2}", "meta": "wrapfig-cmd", "score": 0.0003295435821387379}], "epsfig": [{"caption": "\\epsfbox{}", "snippet": "\\epsfbox{$1}", "meta": "epsfig-cmd", "score": 0.00013712781345832882}, {"caption": "\\psfig{}", "snippet": "\\psfig{$1}", "meta": "epsfig-cmd", "score": 0.0017552046452897515}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "epsfig-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "epsfig-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "epsfig-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "epsfig-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "epsfig-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "epsfig-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "epsfig-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "epsfig-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "epsfig-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epsfig-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "epsfig-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "epsfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "epsfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "epsfig-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "epsfig-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "epsfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "epsfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "epsfig-cmd", "score": 0.004719094298848707}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "epsfig-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epsfig-cmd", "score": 0.008565354665444157}], "cite": [{"caption": "\\citeonline{}", "snippet": "\\citeonline{$1}", "meta": "cite-cmd", "score": 0.014277840409455324}, {"caption": "\\citenum{}", "snippet": "\\citenum{$1}", "meta": "cite-cmd", "score": 0.0027420903627423383}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "cite-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "cite-cmd", "score": 0.021170869458413965}, {"caption": "\\nocite{}", "snippet": "\\nocite{$1}", "meta": "cite-cmd", "score": 0.04990693820960752}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "cite-cmd", "score": 2.341195220791228}], "lipsum": [{"caption": "\\setlipsumdefault{}", "snippet": "\\setlipsumdefault{$1}", "meta": "lipsum-cmd", "score": 0.00024112945034541791}, {"caption": "\\lipsum[]", "snippet": "\\lipsum[$1]", "meta": "lipsum-cmd", "score": 0.0300787181624191}], "algpseudocode": [{"caption": "\\algrenewcommand", "snippet": "\\algrenewcommand", "meta": "algpseudocode-cmd", "score": 0.0019861803661869416}, {"caption": "\\Statex", "snippet": "\\Statex", "meta": "algpseudocode-cmd", "score": 0.008622777195102994}, {"caption": "\\BState{}", "snippet": "\\BState{$1}", "meta": "algpseudocode-cmd", "score": 0.0008685861525307122}, {"caption": "\\BState", "snippet": "\\BState", "meta": "algpseudocode-cmd", "score": 0.0008685861525307122}, {"caption": "\\algloopdefx{}[][]{}", "snippet": "\\algloopdefx{$1}[$2][$3]{$4}", "meta": "algpseudocode-cmd", "score": 0.00025315185701145097}, {"caption": "\\algnewcommand", "snippet": "\\algnewcommand", "meta": "algpseudocode-cmd", "score": 0.0030209395012065327}, {"caption": "\\algnewcommand{}[]{}", "snippet": "\\algnewcommand{$1}[$2]{$3}", "meta": "algpseudocode-cmd", "score": 0.0030209395012065327}, {"caption": "\\Comment{}", "snippet": "\\Comment{$1}", "meta": "algpseudocode-cmd", "score": 0.005178604573219454}, {"caption": "\\algblockdefx{}{}[]", "snippet": "\\algblockdefx{$1}{$2}[$3]", "meta": "algpseudocode-cmd", "score": 0.00025315185701145097}, {"caption": "\\algrenewtext{}{}", "snippet": "\\algrenewtext{$1}{$2}", "meta": "algpseudocode-cmd", "score": 0.0024415580558825975}, {"caption": "\\algrenewtext{}[]{}", "snippet": "\\algrenewtext{$1}[$2]{$3}", "meta": "algpseudocode-cmd", "score": 0.0024415580558825975}, {"caption": "\\algblock{}{}", "snippet": "\\algblock{$1}{$2}", "meta": "algpseudocode-cmd", "score": 0.0007916858220314837}, {"caption": "\\csname", "snippet": "\\csname", "meta": "algpseudocode-cmd", "score": 0.008565354665444157}, {"caption": "\\algdef{}[]{}{}{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "algpseudocode-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}{}[]{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}", "meta": "algpseudocode-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}[]{}", "snippet": "\\algdef{$1}[$2]{$3}[$4]{$5}", "meta": "algpseudocode-cmd", "score": 0.0003102486920966127}, {"caption": "\\algtext{}", "snippet": "\\algtext{$1}", "meta": "algpseudocode-cmd", "score": 0.0005463612015579842}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "algpseudocode-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "algpseudocode-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "algpseudocode-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "algpseudocode-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "algpseudocode-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "algpseudocode-cmd", "score": 0.0018957469739775527}], "textpos": [{"caption": "\\textblockorigin{}{}", "snippet": "\\textblockorigin{$1}{$2}", "meta": "textpos-cmd", "score": 0.016306266556901577}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "textpos-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "textpos-cmd", "score": 0.2864294797053033}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "textpos-cmd", "score": 0.00037306820619479756}], "subfig": [{"caption": "\\subref{}", "snippet": "\\subref{$1}", "meta": "subfig-cmd", "score": 0.007192033516871399}, {"caption": "\\protect", "snippet": "\\protect", "meta": "subfig-cmd", "score": 0.0200686676229443}, {"caption": "\\subfloat[]{}", "snippet": "\\subfloat[$1]{$2}", "meta": "subfig-cmd", "score": 0.0286920437310672}, {"caption": "\\subfloat{}", "snippet": "\\subfloat{$1}", "meta": "subfig-cmd", "score": 0.0286920437310672}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "subfig-cmd", "score": 0.00037306820619479756}], "enumerate": [{"caption": "\\csname", "snippet": "\\csname", "meta": "enumerate-cmd", "score": 0.008565354665444157}, {"caption": "\\makelabel", "snippet": "\\makelabel", "meta": "enumerate-cmd", "score": 5.739925426740175e-05}, {"caption": "\\makelabel{}", "snippet": "\\makelabel{$1}", "meta": "enumerate-cmd", "score": 5.739925426740175e-05}, {"caption": "\\makelabel[]{}", "snippet": "\\makelabel[$1]{$2}", "meta": "enumerate-cmd", "score": 5.739925426740175e-05}], "pdfpages": [{"caption": "\\csname", "snippet": "\\csname", "meta": "pdfpages-cmd", "score": 0.008565354665444157}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "pdfpages-cmd", "score": 0.07503475348393239}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pdfpages-cmd", "score": 1.4595731795525781}, {"caption": "\\includepdf[]{}", "snippet": "\\includepdf[$1]{$2}", "meta": "pdfpages-cmd", "score": 0.023931732745590156}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pdfpages-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "pdfpages-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "pdfpages-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "pdfpages-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pdfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pdfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "pdfpages-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "pdfpages-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "pdfpages-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "pdfpages-cmd", "score": 0.028955796305270766}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pdfpages-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pdfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pdfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "pdfpages-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "pdfpages-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pdfpages-cmd", "score": 0.008565354665444157}, {"caption": "\\AddToShipoutPictureFG{}", "snippet": "\\AddToShipoutPictureFG{$1}", "meta": "pdfpages-cmd", "score": 0.000325977535138643}, {"caption": "\\AddToShipoutPictureBG{}", "snippet": "\\AddToShipoutPictureBG{$1}", "meta": "pdfpages-cmd", "score": 0.0008957666085644653}, {"caption": "\\AtPageUpperLeft{}", "snippet": "\\AtPageUpperLeft{$1}", "meta": "pdfpages-cmd", "score": 0.0003608141410278152}, {"caption": "\\LenToUnit{}", "snippet": "\\LenToUnit{$1}", "meta": "pdfpages-cmd", "score": 0.0007216282820556304}, {"caption": "\\AddToShipoutPicture{}", "snippet": "\\AddToShipoutPicture{$1}", "meta": "pdfpages-cmd", "score": 0.0017658629469099734}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "pdfpages-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "pdfpages-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "pdfpages-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "pdfpages-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "pdfpages-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "pdfpages-cmd", "score": 0.0018957469739775527}], "epstopdf": [{"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\AppendGraphicsExtensions{}", "snippet": "\\AppendGraphicsExtensions{$1}", "meta": "epstopdf-cmd", "score": 7.723677706376668e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "epstopdf-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "epstopdf-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "epstopdf-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "epstopdf-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "epstopdf-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "epstopdf-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "epstopdf-cmd", "score": 0.021170869458413965}, {"caption": "\\epstopdfsetup{}", "snippet": "\\epstopdfsetup{$1}", "meta": "epstopdf-cmd", "score": 0.0009941134326203623}, {"caption": "\\epstopdfDeclareGraphicsRule{}{}{}{}", "snippet": "\\epstopdfDeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "epstopdf-cmd", "score": 7.723677706376668e-05}, {"caption": "\\OutputFile", "snippet": "\\OutputFile", "meta": "epstopdf-cmd", "score": 7.723677706376668e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "epstopdf-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "epstopdf-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "epstopdf-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "epstopdf-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "epstopdf-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epstopdf-cmd", "score": 0.008565354665444157}], "lmodern": [{"caption": "\\rmdefault", "snippet": "\\rmdefault", "meta": "lmodern-cmd", "score": 0.0012870877747432935}, {"caption": "\\sfdefault", "snippet": "\\sfdefault", "meta": "lmodern-cmd", "score": 0.008427383388519996}, {"caption": "\\sfdefault{}", "snippet": "\\sfdefault{$1}", "meta": "lmodern-cmd", "score": 0.008427383388519996}], "pifont": [{"caption": "\\ding{}", "snippet": "\\ding{$1}", "meta": "pifont-cmd", "score": 0.009992300665793867}], "ragged2e": [{"caption": "\\justifying", "snippet": "\\justifying", "meta": "ragged2e-cmd", "score": 0.010373702256548788}, {"caption": "\\justifying{}", "snippet": "\\justifying{$1}", "meta": "ragged2e-cmd", "score": 0.010373702256548788}, {"caption": "\\RaggedRight", "snippet": "\\RaggedRight", "meta": "ragged2e-cmd", "score": 0.001021021782267457}, {"caption": "\\Centering", "snippet": "\\Centering", "meta": "ragged2e-cmd", "score": 0.00037395241488843035}, {"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "ragged2e-cmd", "score": 0.04598628699063736}], "rotating": [{"caption": "\\csname", "snippet": "\\csname", "meta": "rotating-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "rotating-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "rotating-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "rotating-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "rotating-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "rotating-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "rotating-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "rotating-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "rotating-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "rotating-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "rotating-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rotating-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "rotating-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "rotating-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "rotating-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "rotating-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "rotating-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "rotating-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "rotating-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "rotating-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "rotating-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "rotating-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "rotating-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "rotating-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "rotating-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "rotating-cmd", "score": 0.004719094298848707}], "xltxtra": [{"caption": "\\textsubscript{}", "snippet": "\\textsubscript{$1}", "meta": "xltxtra-cmd", "score": 0.058405875394131175}, {"caption": "\\textsuperscript{}", "snippet": "\\textsuperscript{$1}", "meta": "xltxtra-cmd", "score": 0.05216393882408519}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "xltxtra-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xltxtra-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xltxtra-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xltxtra-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xltxtra-cmd", "score": 0.004719094298848707}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "xltxtra-cmd", "score": 0.00021116765384691477}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xltxtra-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xltxtra-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "xltxtra-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "xltxtra-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "xltxtra-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "xltxtra-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "xltxtra-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xltxtra-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "xltxtra-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xltxtra-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "xltxtra-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xltxtra-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xltxtra-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xltxtra-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "xltxtra-cmd", "score": 0.004649150613625593}, {"caption": "\\XeTeX", "snippet": "\\XeTeX", "meta": "xltxtra-cmd", "score": 0.0010635559050357936}, {"caption": "\\TeX", "snippet": "\\TeX", "meta": "xltxtra-cmd", "score": 0.02873756018238537}, {"caption": "\\TeX{}", "snippet": "\\TeX{$1}", "meta": "xltxtra-cmd", "score": 0.02873756018238537}, {"caption": "\\LaTeX", "snippet": "\\LaTeX", "meta": "xltxtra-cmd", "score": 0.2334089308452787}, {"caption": "\\LaTeX{}", "snippet": "\\LaTeX{$1}", "meta": "xltxtra-cmd", "score": 0.2334089308452787}, {"caption": "\\XeLaTeX", "snippet": "\\XeLaTeX", "meta": "xltxtra-cmd", "score": 0.002009786035379175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xltxtra-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xltxtra-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xltxtra-cmd", "score": 0.2864294797053033}], "marvosym": [{"caption": "\\Mundus", "snippet": "\\Mundus", "meta": "marvosym-cmd", "score": 0.0006349134235582933}, {"caption": "\\Telefon", "snippet": "\\Telefon", "meta": "marvosym-cmd", "score": 0.0003618274070138519}, {"caption": "\\Letter", "snippet": "\\Letter", "meta": "marvosym-cmd", "score": 0.0012281130571092198}, {"caption": "\\Mobilefone", "snippet": "\\Mobilefone", "meta": "marvosym-cmd", "score": 0.0005432037068220953}], "dcolumn": [{"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "dcolumn-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "dcolumn-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "dcolumn-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "dcolumn-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "dcolumn-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "dcolumn-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "dcolumn-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "dcolumn-cmd", "score": 0.018615449342361392}], "xspace": [{"caption": "\\xspace", "snippet": "\\xspace", "meta": "xspace-cmd", "score": 0.07560370351316588}], "xunicode": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xunicode-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xunicode-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xunicode-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xunicode-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "xunicode-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "xunicode-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "xunicode-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "xunicode-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "xunicode-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xunicode-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "xunicode-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xunicode-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "xunicode-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xunicode-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xunicode-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xunicode-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "xunicode-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xunicode-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xunicode-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xunicode-cmd", "score": 0.004719094298848707}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "xunicode-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xunicode-cmd", "score": 0.008565354665444157}], "csquotes": [{"caption": "\\mkcitation", "snippet": "\\mkcitation", "meta": "csquotes-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\DeclareQuoteAlias{}{}", "snippet": "\\DeclareQuoteAlias{$1}{$2}", "meta": "csquotes-cmd", "score": 0.0004906235524176374}, {"caption": "\\quote{}", "snippet": "\\quote{$1}", "meta": "csquotes-cmd", "score": 0.030690393112264815}, {"caption": "\\quote", "snippet": "\\quote", "meta": "csquotes-cmd", "score": 0.030690393112264815}, {"caption": "\\setquotestyle[]{}", "snippet": "\\setquotestyle[$1]{$2}", "meta": "csquotes-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\blockquote{}", "snippet": "\\blockquote{$1}", "meta": "csquotes-cmd", "score": 0.00023365626458085812}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "csquotes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "csquotes-cmd", "score": 0.021170869458413965}, {"caption": "\\mkbegdispquote", "snippet": "\\mkbegdispquote", "meta": "csquotes-cmd", "score": 4.203362017075738e-05}, {"caption": "\\do", "snippet": "\\do", "meta": "csquotes-cmd", "score": 0.009278344180101056}, {"caption": "\\break", "snippet": "\\break", "meta": "csquotes-cmd", "score": 0.016352452390960115}, {"caption": "\\break{}", "snippet": "\\break{$1}", "meta": "csquotes-cmd", "score": 0.016352452390960115}, {"caption": "\\break{}{}", "snippet": "\\break{$1}{$2}", "meta": "csquotes-cmd", "score": 0.016352452390960115}, {"caption": "\\ifpunctmark{}", "snippet": "\\ifpunctmark{$1}", "meta": "csquotes-cmd", "score": 7.723677706376668e-05}, {"caption": "\\endquote", "snippet": "\\endquote", "meta": "csquotes-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\par", "snippet": "\\par", "meta": "csquotes-cmd", "score": 0.413853376001159}, {"caption": "\\DeclareQuoteStyle[]{}", "snippet": "\\DeclareQuoteStyle[$1]{$2}", "meta": "csquotes-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\SetBlockEnvironment{}", "snippet": "\\SetBlockEnvironment{$1}", "meta": "csquotes-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "csquotes-cmd", "score": 0.008565354665444157}, {"caption": "\\MakeOuterQuote{}", "snippet": "\\MakeOuterQuote{$1}", "meta": "csquotes-cmd", "score": 0.0019170811203505262}, {"caption": "\\enquote{}", "snippet": "\\enquote{$1}", "meta": "csquotes-cmd", "score": 0.0077432730806830915}, {"caption": "\\SetCiteCommand{}", "snippet": "\\SetCiteCommand{$1}", "meta": "csquotes-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "csquotes-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "csquotes-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "csquotes-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "csquotes-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "csquotes-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "csquotes-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "csquotes-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "csquotes-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "csquotes-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "csquotes-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "csquotes-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "csquotes-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "csquotes-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "csquotes-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "csquotes-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "csquotes-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "csquotes-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "csquotes-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "csquotes-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "csquotes-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "csquotes-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "csquotes-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "csquotes-cmd", "score": 0.00037306820619479756}], "xparse": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xparse-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xparse-cmd", "score": 0.2864294797053033}], "soul": [{"caption": "\\DeclareRobustCommand{}{}", "snippet": "\\DeclareRobustCommand{$1}{$2}", "meta": "soul-cmd", "score": 0.0010373158471650705}, {"caption": "\\DeclareRobustCommand{}[]{}", "snippet": "\\DeclareRobustCommand{$1}[$2]{$3}", "meta": "soul-cmd", "score": 0.0010373158471650705}, {"caption": "\\sethlcolor{}", "snippet": "\\sethlcolor{$1}", "meta": "soul-cmd", "score": 0.01970230898277056}, {"caption": "\\st", "snippet": "\\st", "meta": "soul-cmd", "score": 0.004652662833362787}, {"caption": "\\st{}", "snippet": "\\st{$1}", "meta": "soul-cmd", "score": 0.004652662833362787}, {"caption": "\\def", "snippet": "\\def", "meta": "soul-cmd", "score": 0.21357759092476175}, {"caption": "\\hl{}", "snippet": "\\hl{$1}", "meta": "soul-cmd", "score": 0.03421486301062431}, {"caption": "\\sodef", "snippet": "\\sodef", "meta": "soul-cmd", "score": 0.0017045357696831268}, {"caption": "\\csname", "snippet": "\\csname", "meta": "soul-cmd", "score": 0.008565354665444157}, {"caption": "\\so", "snippet": "\\so", "meta": "soul-cmd", "score": 0.004308800134587786}, {"caption": "\\so{}", "snippet": "\\so{$1}", "meta": "soul-cmd", "score": 0.004308800134587786}], "comment": [{"caption": "\\specialcomment{}{}{}", "snippet": "\\specialcomment{$1}{$2}{$3}", "meta": "comment-cmd", "score": 9.120209837787948e-05}, {"caption": "\\includecomment{}", "snippet": "\\includecomment{$1}", "meta": "comment-cmd", "score": 8.21804444236254e-05}], "algorithm2e": [{"caption": "\\FuncSty{}", "snippet": "\\FuncSty{$1}", "meta": "algorithm2e-cmd", "score": 7.576875738934807e-05}, {"caption": "\\algorithmautorefname", "snippet": "\\algorithmautorefname", "meta": "algorithm2e-cmd", "score": 2.0085955839419213e-05}, {"caption": "\\SetAlgoNoLine", "snippet": "\\SetAlgoNoLine", "meta": "algorithm2e-cmd", "score": 0.00015722499147840545}, {"caption": "\\Indp", "snippet": "\\Indp", "meta": "algorithm2e-cmd", "score": 6.068942580823901e-05}, {"caption": "\\AlCapFnt", "snippet": "\\AlCapFnt", "meta": "algorithm2e-cmd", "score": 3.0307502955739227e-05}, {"caption": "\\LinesNumbered", "snippet": "\\LinesNumbered", "meta": "algorithm2e-cmd", "score": 0.000162125616653719}, {"caption": "\\SetAlFnt{}", "snippet": "\\SetAlFnt{$1}", "meta": "algorithm2e-cmd", "score": 0.0024446198714390757}, {"caption": "\\SetKw{}{}", "snippet": "\\SetKw{$1}{$2}", "meta": "algorithm2e-cmd", "score": 9.292434841280213e-05}, {"caption": "\\RestyleAlgo{}", "snippet": "\\RestyleAlgo{$1}", "meta": "algorithm2e-cmd", "score": 0.00019243311960945823}, {"caption": "\\listofalgorithms", "snippet": "\\listofalgorithms", "meta": "algorithm2e-cmd", "score": 0.0012576983422794912}, {"caption": "\\IncMargin{}", "snippet": "\\IncMargin{$1}", "meta": "algorithm2e-cmd", "score": 0.0024294661199612063}, {"caption": "\\BlankLine", "snippet": "\\BlankLine", "meta": "algorithm2e-cmd", "score": 0.005049617303688214}, {"caption": "\\SetCommentSty{}", "snippet": "\\SetCommentSty{$1}", "meta": "algorithm2e-cmd", "score": 0.0001778112853266571}, {"caption": "\\SetAlgoNoEnd", "snippet": "\\SetAlgoNoEnd", "meta": "algorithm2e-cmd", "score": 0.00015722499147840545}, {"caption": "\\theAlgoLine{}", "snippet": "\\theAlgoLine{$1}", "meta": "algorithm2e-cmd", "score": 1.5153751477869614e-05}, {"caption": "\\SetKwBlock{}{}{}", "snippet": "\\SetKwBlock{$1}{$2}{$3}", "meta": "algorithm2e-cmd", "score": 0.000981463850523159}, {"caption": "\\SetKwBlock{}{}", "snippet": "\\SetKwBlock{$1}{$2}", "meta": "algorithm2e-cmd", "score": 0.000981463850523159}, {"caption": "\\AlCapNameFnt", "snippet": "\\AlCapNameFnt", "meta": "algorithm2e-cmd", "score": 3.0307502955739227e-05}, {"caption": "\\SetAlgoSkip{}", "snippet": "\\SetAlgoSkip{$1}", "meta": "algorithm2e-cmd", "score": 0.00017454032258926576}, {"caption": "\\SetKwFunction{}{}", "snippet": "\\SetKwFunction{$1}{$2}", "meta": "algorithm2e-cmd", "score": 0.0015332307832994817}, {"caption": "\\nllabel{}", "snippet": "\\nllabel{$1}", "meta": "algorithm2e-cmd", "score": 0.0001844460347791443}, {"caption": "\\SetAlgoInsideSkip{}", "snippet": "\\SetAlgoInsideSkip{$1}", "meta": "algorithm2e-cmd", "score": 4.5812360816321294e-05}, {"caption": "\\DataSty{}", "snippet": "\\DataSty{$1}", "meta": "algorithm2e-cmd", "score": 1.5153751477869614e-05}, {"caption": "\\SetKwInOut{}{}", "snippet": "\\SetKwInOut{$1}{$2}", "meta": "algorithm2e-cmd", "score": 0.0017021978326807814}, {"caption": "\\SetAlCapFnt{}", "snippet": "\\SetAlCapFnt{$1}", "meta": "algorithm2e-cmd", "score": 0.0024294661199612063}, {"caption": "\\CommentSty{}", "snippet": "\\CommentSty{$1}", "meta": "algorithm2e-cmd", "score": 0.0001111448631633176}, {"caption": "\\SetAlCapHSkip{}", "snippet": "\\SetAlCapHSkip{$1}", "meta": "algorithm2e-cmd", "score": 0.0024294661199612063}, {"caption": "\\renewcommand{}{}", "snippet": "\\renewcommand{$1}{$2}", "meta": "algorithm2e-cmd", "score": 0.3267437011085663}, {"caption": "\\renewcommand", "snippet": "\\renewcommand", "meta": "algorithm2e-cmd", "score": 0.3267437011085663}, {"caption": "\\algorithmcfname", "snippet": "\\algorithmcfname", "meta": "algorithm2e-cmd", "score": 0.0024445413067013134}, {"caption": "\\SetKwIF{}{}{}{}{}{}{}{}", "snippet": "\\SetKwIF{$1}{$2}{$3}{$4}{$5}{$6}{$7}{$8}", "meta": "algorithm2e-cmd", "score": 1.5153751477869614e-05}, {"caption": "\\SetAlgoCaptionSeparator{}", "snippet": "\\SetAlgoCaptionSeparator{$1}", "meta": "algorithm2e-cmd", "score": 1.5153751477869614e-05}, {"caption": "\\AlCapSty{}", "snippet": "\\AlCapSty{$1}", "meta": "algorithm2e-cmd", "score": 3.0307502955739227e-05}, {"caption": "\\ArgSty{}", "snippet": "\\ArgSty{$1}", "meta": "algorithm2e-cmd", "score": 3.0307502955739227e-05}, {"caption": "\\AlCapNameSty{}", "snippet": "\\AlCapNameSty{$1}", "meta": "algorithm2e-cmd", "score": 3.0307502955739227e-05}, {"caption": "\\SetKwData{}{}", "snippet": "\\SetKwData{$1}{$2}", "meta": "algorithm2e-cmd", "score": 0.00235652682860263}, {"caption": "\\listalgorithmcfname", "snippet": "\\listalgorithmcfname", "meta": "algorithm2e-cmd", "score": 1.5075186740106946e-05}, {"caption": "\\Indm", "snippet": "\\Indm", "meta": "algorithm2e-cmd", "score": 6.068942580823901e-05}, {"caption": "\\SetAlCapNameFnt{}", "snippet": "\\SetAlCapNameFnt{$1}", "meta": "algorithm2e-cmd", "score": 0.0024294661199612063}, {"caption": "\\DontPrintSemicolon", "snippet": "\\DontPrintSemicolon", "meta": "algorithm2e-cmd", "score": 0.001062087490197768}, {"caption": "\\SetAlgoLined", "snippet": "\\SetAlgoLined", "meta": "algorithm2e-cmd", "score": 0.0017151361342403852}, {"caption": "\\SetAlCapSkip{}", "snippet": "\\SetAlCapSkip{$1}", "meta": "algorithm2e-cmd", "score": 0.0006213942502400296}, {"caption": "\\LinesNotNumbered", "snippet": "\\LinesNotNumbered", "meta": "algorithm2e-cmd", "score": 1.5153751477869614e-05}, {"caption": "\\SetKwProg{}{}{}{}", "snippet": "\\SetKwProg{$1}{$2}{$3}{$4}", "meta": "algorithm2e-cmd", "score": 0.0008518783278391971}, {"caption": "\\SetAlgoVlined", "snippet": "\\SetAlgoVlined", "meta": "algorithm2e-cmd", "score": 1.5153751477869614e-05}, {"caption": "\\SetKwRepeat{}{}{}", "snippet": "\\SetKwRepeat{$1}{$2}{$3}", "meta": "algorithm2e-cmd", "score": 6.110202388233705e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "algorithm2e-cmd", "score": 0.008565354665444157}, {"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "algorithm2e-cmd", "score": 0.422097569591803}, {"caption": "\\SetKwFor{}{}{}{}", "snippet": "\\SetKwFor{$1}{$2}{$3}{$4}", "meta": "algorithm2e-cmd", "score": 0.00010699539949594301}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "algorithm2e-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "algorithm2e-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "algorithm2e-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "algorithm2e-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "algorithm2e-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "algorithm2e-cmd", "score": 0.0018957469739775527}, {"caption": "\\xspace", "snippet": "\\xspace", "meta": "algorithm2e-cmd", "score": 0.07560370351316588}], "tocbibind": [{"caption": "\\contentsname", "snippet": "\\contentsname", "meta": "tocbibind-cmd", "score": 0.010205180337548728}, {"caption": "\\contentsname{}", "snippet": "\\contentsname{$1}", "meta": "tocbibind-cmd", "score": 0.010205180337548728}, {"caption": "\\tocchapter", "snippet": "\\tocchapter", "meta": "tocbibind-cmd", "score": 0.00016023188758771694}, {"caption": "\\indexname", "snippet": "\\indexname", "meta": "tocbibind-cmd", "score": 0.0007544109314450072}, {"caption": "\\listoffigures", "snippet": "\\listoffigures", "meta": "tocbibind-cmd", "score": 0.03447318897846567}, {"caption": "\\tocfile{}{}", "snippet": "\\tocfile{$1}{$2}", "meta": "tocbibind-cmd", "score": 0.00016023188758771694}, {"caption": "\\tocbibname", "snippet": "\\tocbibname", "meta": "tocbibind-cmd", "score": 0.0020762574479507175}, {"caption": "\\settocbibname{}", "snippet": "\\settocbibname{$1}", "meta": "tocbibind-cmd", "score": 0.00010668677119599426}, {"caption": "\\listoftables", "snippet": "\\listoftables", "meta": "tocbibind-cmd", "score": 0.02104656820469027}, {"caption": "\\tableofcontents", "snippet": "\\tableofcontents", "meta": "tocbibind-cmd", "score": 0.13360595130994957}, {"caption": "\\listfigurename", "snippet": "\\listfigurename", "meta": "tocbibind-cmd", "score": 0.0034407237779350256}], "pgfplots": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfplots-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfplots-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfplots-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfplots-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfplots-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfplots-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfplots-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfplots-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfplots-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfplots-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfplots-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfplots-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfplots-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfplots-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfplots-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfplots-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfplots-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfplots-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfplots-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfplots-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfplots-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfplots-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfplots-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfplots-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfplots-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfplots-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfplots-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfplots-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfplots-cmd", "score": 0.2864294797053033}], "lastpage": [{"caption": "\\string", "snippet": "\\string", "meta": "lastpage-cmd", "score": 0.001042697111754002}], "graphics": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "graphics-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "graphics-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "graphics-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "graphics-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "graphics-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "graphics-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "graphics-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "graphics-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "graphics-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "graphics-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "graphics-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "graphics-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "graphics-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "graphics-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "graphics-cmd", "score": 0.004649150613625593}, {"caption": "\\csname", "snippet": "\\csname", "meta": "graphics-cmd", "score": 0.008565354665444157}], "algorithmic": [{"caption": "\\REPEAT", "snippet": "\\REPEAT", "meta": "algorithmic-cmd", "score": 0.0004816110638193742}, {"caption": "\\ENDIF", "snippet": "\\ENDIF", "meta": "algorithmic-cmd", "score": 0.003585213685098552}, {"caption": "\\algorithmicwhile", "snippet": "\\algorithmicwhile", "meta": "algorithmic-cmd", "score": 0.0005769483780443573}, {"caption": "\\algorithmicwhile{}", "snippet": "\\algorithmicwhile{$1}", "meta": "algorithmic-cmd", "score": 0.0005769483780443573}, {"caption": "\\FOR{}", "snippet": "\\FOR{$1}", "meta": "algorithmic-cmd", "score": 0.004074774218819945}, {"caption": "\\algorithmicif", "snippet": "\\algorithmicif", "meta": "algorithmic-cmd", "score": 0.00039654130753044966}, {"caption": "\\algorithmicif{}", "snippet": "\\algorithmicif{$1}", "meta": "algorithmic-cmd", "score": 0.00039654130753044966}, {"caption": "\\ENDFOR", "snippet": "\\ENDFOR", "meta": "algorithmic-cmd", "score": 0.004428141530092572}, {"caption": "\\UNTIL", "snippet": "\\UNTIL", "meta": "algorithmic-cmd", "score": 0.0004816110638193742}, {"caption": "\\UNTIL{}", "snippet": "\\UNTIL{$1}", "meta": "algorithmic-cmd", "score": 0.0004816110638193742}, {"caption": "\\IF{}", "snippet": "\\IF{$1}", "meta": "algorithmic-cmd", "score": 0.0036985887706967417}, {"caption": "\\ENSURE", "snippet": "\\ENSURE", "meta": "algorithmic-cmd", "score": 0.0013188761425395954}, {"caption": "\\algorithmiccomment", "snippet": "\\algorithmiccomment", "meta": "algorithmic-cmd", "score": 0.00021737766481978388}, {"caption": "\\ENDWHILE", "snippet": "\\ENDWHILE", "meta": "algorithmic-cmd", "score": 0.00047037943460091465}, {"caption": "\\algorithmicend", "snippet": "\\algorithmicend", "meta": "algorithmic-cmd", "score": 0.0011128218085672747}, {"caption": "\\algorithmicend{}", "snippet": "\\algorithmicend{$1}", "meta": "algorithmic-cmd", "score": 0.0011128218085672747}, {"caption": "\\algorithmicrequire", "snippet": "\\algorithmicrequire", "meta": "algorithmic-cmd", "score": 0.004751598472180266}, {"caption": "\\algorithmicdo", "snippet": "\\algorithmicdo", "meta": "algorithmic-cmd", "score": 0.0005655570358533174}, {"caption": "\\algorithmicdo{}", "snippet": "\\algorithmicdo{$1}", "meta": "algorithmic-cmd", "score": 0.0005655570358533174}, {"caption": "\\algorithmicfor", "snippet": "\\algorithmicfor", "meta": "algorithmic-cmd", "score": 0.0005681785898943757}, {"caption": "\\algorithmicfor{}", "snippet": "\\algorithmicfor{$1}", "meta": "algorithmic-cmd", "score": 0.0005681785898943757}, {"caption": "\\RETURN", "snippet": "\\RETURN", "meta": "algorithmic-cmd", "score": 0.0013054907995767408}, {"caption": "\\algorithmicand", "snippet": "\\algorithmicand", "meta": "algorithmic-cmd", "score": 5.326674280259771e-05}, {"caption": "\\algsetup{}", "snippet": "\\algsetup{$1}", "meta": "algorithmic-cmd", "score": 0.00012872796177294446}, {"caption": "\\algorithmicreturn{}", "snippet": "\\algorithmicreturn{$1}", "meta": "algorithmic-cmd", "score": 0.00022490402516652368}, {"caption": "\\algorithmicreturn", "snippet": "\\algorithmicreturn", "meta": "algorithmic-cmd", "score": 0.00022490402516652368}, {"caption": "\\algorithmicforall{}", "snippet": "\\algorithmicforall{$1}", "meta": "algorithmic-cmd", "score": 0.00022490402516652368}, {"caption": "\\algorithmicforall", "snippet": "\\algorithmicforall", "meta": "algorithmic-cmd", "score": 0.00022490402516652368}, {"caption": "\\COMMENT", "snippet": "\\COMMENT", "meta": "algorithmic-cmd", "score": 0.00025669572555354604}, {"caption": "\\COMMENT{}", "snippet": "\\COMMENT{$1}", "meta": "algorithmic-cmd", "score": 0.00025669572555354604}, {"caption": "\\REQUIRE", "snippet": "\\REQUIRE", "meta": "algorithmic-cmd", "score": 0.001870681168192269}, {"caption": "\\algorithmicor", "snippet": "\\algorithmicor", "meta": "algorithmic-cmd", "score": 5.326674280259771e-05}, {"caption": "\\ELSE", "snippet": "\\ELSE", "meta": "algorithmic-cmd", "score": 0.0007599864146830139}, {"caption": "\\STATE", "snippet": "\\STATE", "meta": "algorithmic-cmd", "score": 0.0266684860947573}, {"caption": "\\WHILE{}", "snippet": "\\WHILE{$1}", "meta": "algorithmic-cmd", "score": 0.00047037943460091465}, {"caption": "\\ELSIF{}", "snippet": "\\ELSIF{$1}", "meta": "algorithmic-cmd", "score": 0.0001991613148371481}, {"caption": "\\FALSE", "snippet": "\\FALSE", "meta": "algorithmic-cmd", "score": 3.34222699937868e-05}, {"caption": "\\AND", "snippet": "\\AND", "meta": "algorithmic-cmd", "score": 6.401730289932545e-05}, {"caption": "\\algorithmicensure", "snippet": "\\algorithmicensure", "meta": "algorithmic-cmd", "score": 0.003439482525198322}, {"caption": "\\OR", "snippet": "\\OR", "meta": "algorithmic-cmd", "score": 6.401730289932545e-05}, {"caption": "\\algorithmicrepeat", "snippet": "\\algorithmicrepeat", "meta": "algorithmic-cmd", "score": 5.326674280259771e-05}, {"caption": "\\TRUE", "snippet": "\\TRUE", "meta": "algorithmic-cmd", "score": 0.0001336890799751472}, {"caption": "\\FORALL{}", "snippet": "\\FORALL{$1}", "meta": "algorithmic-cmd", "score": 0.0003533673112726266}, {"caption": "\\algorithmicthen{}", "snippet": "\\algorithmicthen{$1}", "meta": "algorithmic-cmd", "score": 0.00032476571672371697}, {"caption": "\\algorithmicthen", "snippet": "\\algorithmicthen", "meta": "algorithmic-cmd", "score": 0.00032476571672371697}, {"caption": "\\algorithmicuntil", "snippet": "\\algorithmicuntil", "meta": "algorithmic-cmd", "score": 5.326674280259771e-05}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "algorithmic-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "algorithmic-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "algorithmic-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "algorithmic-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "algorithmic-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "algorithmic-cmd", "score": 0.0018957469739775527}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "algorithmic-cmd", "score": 0.00037306820619479756}], "lineno": [{"caption": "\\pagewiselinenumbers", "snippet": "\\pagewiselinenumbers", "meta": "lineno-cmd", "score": 0.00016870831850106035}, {"caption": "\\linenomath", "snippet": "\\linenomath", "meta": "lineno-cmd", "score": 1.4517338420208715e-05}, {"caption": "\\linenumberfont{}", "snippet": "\\linenumberfont{$1}", "meta": "lineno-cmd", "score": 0.0001811784338695797}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "lineno-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "lineno-cmd", "score": 0.021170869458413965}, {"caption": "\\endlinenomath", "snippet": "\\endlinenomath", "meta": "lineno-cmd", "score": 1.4517338420208715e-05}, {"caption": "\\nolinenumbers", "snippet": "\\nolinenumbers", "meta": "lineno-cmd", "score": 0.0009805246614299932}, {"caption": "\\path", "snippet": "\\path", "meta": "lineno-cmd", "score": 0.028200474217322108}, {"caption": "\\path[]", "snippet": "\\path[$1]", "meta": "lineno-cmd", "score": 0.028200474217322108}, {"caption": "\\path{}", "snippet": "\\path{$1}", "meta": "lineno-cmd", "score": 0.028200474217322108}, {"caption": "\\filedate{}", "snippet": "\\filedate{$1}", "meta": "lineno-cmd", "score": 0.000578146635331119}, {"caption": "\\filedate", "snippet": "\\filedate", "meta": "lineno-cmd", "score": 0.000578146635331119}, {"caption": "\\linenumbers", "snippet": "\\linenumbers", "meta": "lineno-cmd", "score": 0.004687680659497865}, {"caption": "\\modulolinenumbers[]", "snippet": "\\modulolinenumbers[$1]", "meta": "lineno-cmd", "score": 0.0027194991933605197}, {"caption": "\\fileversion{}", "snippet": "\\fileversion{$1}", "meta": "lineno-cmd", "score": 0.000578146635331119}, {"caption": "\\fileversion", "snippet": "\\fileversion", "meta": "lineno-cmd", "score": 0.000578146635331119}, {"caption": "\\csname", "snippet": "\\csname", "meta": "lineno-cmd", "score": 0.008565354665444157}], "mathptmx": [{"caption": "\\rmdefault", "snippet": "\\rmdefault", "meta": "mathptmx-cmd", "score": 0.0012870877747432935}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "mathptmx-cmd", "score": 0.04318078602869565}, {"caption": "\\Big", "snippet": "\\Big", "meta": "mathptmx-cmd", "score": 0.050370758781422345}, {"caption": "\\big", "snippet": "\\big", "meta": "mathptmx-cmd", "score": 0.05613164277964739}], "todonotes": [{"caption": "\\missingfigure[]{}", "snippet": "\\missingfigure[$1]{$2}", "meta": "todonotes-cmd", "score": 0.001558719179721163}, {"caption": "\\missingfigure", "snippet": "\\missingfigure", "meta": "todonotes-cmd", "score": 0.001558719179721163}, {"caption": "\\todototoc", "snippet": "\\todototoc", "meta": "todonotes-cmd", "score": 0.000325977535138643}, {"caption": "\\todo{}", "snippet": "\\todo{$1}", "meta": "todonotes-cmd", "score": 0.04115074278362878}, {"caption": "\\todo[]{}", "snippet": "\\todo[$1]{$2}", "meta": "todonotes-cmd", "score": 0.04115074278362878}, {"caption": "\\todo", "snippet": "\\todo", "meta": "todonotes-cmd", "score": 0.04115074278362878}, {"caption": "\\listoftodos", "snippet": "\\listoftodos", "meta": "todonotes-cmd", "score": 0.0005325975940754609}, {"caption": "\\listoftodos[]", "snippet": "\\listoftodos[$1]", "meta": "todonotes-cmd", "score": 0.0005325975940754609}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "todonotes-cmd", "score": 0.0174633138331273}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "todonotes-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "todonotes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "todonotes-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "todonotes-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "todonotes-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "todonotes-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "todonotes-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "todonotes-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "todonotes-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "todonotes-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "todonotes-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "todonotes-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "todonotes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "todonotes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "todonotes-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "todonotes-cmd", "score": 0.004649150613625593}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "todonotes-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "todonotes-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "todonotes-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "todonotes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "todonotes-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "todonotes-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "todonotes-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "todonotes-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "todonotes-cmd", "score": 0.028955796305270766}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "todonotes-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "todonotes-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "todonotes-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "todonotes-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "todonotes-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "todonotes-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "todonotes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "todonotes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "todonotes-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "todonotes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "todonotes-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "todonotes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "todonotes-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "todonotes-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "todonotes-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "todonotes-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "todonotes-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "todonotes-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "todonotes-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "todonotes-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "todonotes-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "todonotes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "todonotes-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "todonotes-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "todonotes-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "todonotes-cmd", "score": 0.2864294797053033}], "ulem": [{"caption": "\\sout{}", "snippet": "\\sout{$1}", "meta": "ulem-cmd", "score": 0.0010443313503631364}, {"caption": "\\sout", "snippet": "\\sout", "meta": "ulem-cmd", "score": 0.0010443313503631364}, {"caption": "\\MakeRobust", "snippet": "\\MakeRobust", "meta": "ulem-cmd", "score": 3.140504277052775e-05}, {"caption": "\\hss", "snippet": "\\hss", "meta": "ulem-cmd", "score": 0.0020627882815078768}, {"caption": "\\uline{}", "snippet": "\\uline{$1}", "meta": "ulem-cmd", "score": 0.005956273219192909}, {"caption": "\\uline", "snippet": "\\uline", "meta": "ulem-cmd", "score": 0.005956273219192909}, {"caption": "\\markoverwith{}", "snippet": "\\markoverwith{$1}", "meta": "ulem-cmd", "score": 0.0004888431085285657}, {"caption": "\\iff", "snippet": "\\iff", "meta": "ulem-cmd", "score": 0.004209937150980285}, {"caption": "\\hfill", "snippet": "\\hfill", "meta": "ulem-cmd", "score": 0.2058248088519886}, {"caption": "\\ULon", "snippet": "\\ULon", "meta": "ulem-cmd", "score": 0.0004888431085285657}, {"caption": "\\normalem", "snippet": "\\normalem", "meta": "ulem-cmd", "score": 0.00015564484081028078}, {"caption": "\\useunder{}{}{}", "snippet": "\\useunder{$1}{$2}{$3}", "meta": "ulem-cmd", "score": 0.0013185833851097916}, {"caption": "\\hfil", "snippet": "\\hfil", "meta": "ulem-cmd", "score": 0.006880789969115855}, {"caption": "\\sout{}", "snippet": "\\sout{$1}", "meta": "ulem-cmd", "score": 0.0010443313503631364}, {"caption": "\\sout", "snippet": "\\sout", "meta": "ulem-cmd", "score": 0.0010443313503631364}, {"caption": "\\MakeRobust", "snippet": "\\MakeRobust", "meta": "ulem-cmd", "score": 3.140504277052775e-05}, {"caption": "\\hss", "snippet": "\\hss", "meta": "ulem-cmd", "score": 0.0020627882815078768}, {"caption": "\\uline{}", "snippet": "\\uline{$1}", "meta": "ulem-cmd", "score": 0.005956273219192909}, {"caption": "\\uline", "snippet": "\\uline", "meta": "ulem-cmd", "score": 0.005956273219192909}, {"caption": "\\markoverwith{}", "snippet": "\\markoverwith{$1}", "meta": "ulem-cmd", "score": 0.0004888431085285657}, {"caption": "\\iff", "snippet": "\\iff", "meta": "ulem-cmd", "score": 0.004209937150980285}, {"caption": "\\hfill", "snippet": "\\hfill", "meta": "ulem-cmd", "score": 0.2058248088519886}, {"caption": "\\ULon", "snippet": "\\ULon", "meta": "ulem-cmd", "score": 0.0004888431085285657}, {"caption": "\\normalem", "snippet": "\\normalem", "meta": "ulem-cmd", "score": 0.00015564484081028078}, {"caption": "\\useunder{}{}{}", "snippet": "\\useunder{$1}{$2}{$3}", "meta": "ulem-cmd", "score": 0.0013185833851097916}, {"caption": "\\hfil", "snippet": "\\hfil", "meta": "ulem-cmd", "score": 0.006880789969115855}], "gensymb": [{"caption": "\\degree", "snippet": "\\degree", "meta": "gensymb-cmd", "score": 0.044752043138360405}, {"caption": "\\ohm", "snippet": "\\ohm", "meta": "gensymb-cmd", "score": 0.0038146685721293138}, {"caption": "\\micro", "snippet": "\\micro", "meta": "gensymb-cmd", "score": 0.011051971930487929}, {"caption": "\\celsius", "snippet": "\\celsius", "meta": "gensymb-cmd", "score": 0.0010806983851157788}], "siunitx": [{"caption": "\\DeclareSIUnit{}{}", "snippet": "\\DeclareSIUnit{$1}{$2}", "meta": "siunitx-cmd", "score": 0.00017911905960739648}, {"caption": "\\DeclareSIUnit", "snippet": "\\DeclareSIUnit", "meta": "siunitx-cmd", "score": 0.00017911905960739648}, {"caption": "\\si{}", "snippet": "\\si{$1}", "meta": "siunitx-cmd", "score": 0.015042996547458706}, {"caption": "\\num{}", "snippet": "\\num{$1}", "meta": "siunitx-cmd", "score": 0.0005077454796577224}, {"caption": "\\num[]{}", "snippet": "\\num[$1]{$2}", "meta": "siunitx-cmd", "score": 0.0005077454796577224}, {"caption": "\\ang{}", "snippet": "\\ang{$1}", "meta": "siunitx-cmd", "score": 0.00026216419341458844}, {"caption": "\\SIrange{}{}{}", "snippet": "\\SIrange{$1}{$2}{$3}", "meta": "siunitx-cmd", "score": 0.0004920776847142836}, {"caption": "\\SIrange[]{}{}{}", "snippet": "\\SIrange[$1]{$2}{$3}{$4}", "meta": "siunitx-cmd", "score": 0.0004920776847142836}, {"caption": "\\SIlist{}{}", "snippet": "\\SIlist{$1}{$2}", "meta": "siunitx-cmd", "score": 2.5005836362206937e-05}, {"caption": "\\SI{}{}", "snippet": "\\SI{$1}{$2}", "meta": "siunitx-cmd", "score": 0.04233098901537305}, {"caption": "\\sisetup{}", "snippet": "\\sisetup{$1}", "meta": "siunitx-cmd", "score": 0.0011875061630332172}, {"caption": "\\do", "snippet": "\\do", "meta": "siunitx-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "siunitx-cmd", "score": 0.0063276692758974925}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "siunitx-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "siunitx-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "siunitx-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "siunitx-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "siunitx-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "siunitx-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "siunitx-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "siunitx-cmd", "score": 0.018615449342361392}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "siunitx-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "siunitx-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "siunitx-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "siunitx-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "siunitx-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "siunitx-cmd", "score": 0.2864294797053033}], "adjustbox": [{"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "adjustbox-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "adjustbox-cmd", "score": 0.354445763583904}, {"caption": "\\adjustbox{}{}", "snippet": "\\adjustbox{$1}{$2}", "meta": "adjustbox-cmd", "score": 0.002008185536556013}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "adjustbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "adjustbox-cmd", "score": 0.021170869458413965}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "adjustbox-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "adjustbox-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "adjustbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "adjustbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "adjustbox-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "adjustbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "adjustbox-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "adjustbox-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "adjustbox-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "adjustbox-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "adjustbox-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "adjustbox-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "adjustbox-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "adjustbox-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "adjustbox-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "adjustbox-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "adjustbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "adjustbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "adjustbox-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "adjustbox-cmd", "score": 0.004649150613625593}], "moderncvcompatibility": [{"caption": "\\cvitem{}{}", "snippet": "\\cvitem{$1}{$2}", "meta": "moderncvcompatibility-cmd", "score": 0.19605476980016281}, {"caption": "\\cvlanguage{}{}{}", "snippet": "\\cvlanguage{$1}{$2}{$3}", "meta": "moderncvcompatibility-cmd", "score": 0.00832363305853651}, {"caption": "\\moderncvtheme[]{}", "snippet": "\\moderncvtheme[$1]{$2}", "meta": "moderncvcompatibility-cmd", "score": 0.002355125248305291}, {"caption": "\\moderncvtheme{}", "snippet": "\\moderncvtheme{$1}", "meta": "moderncvcompatibility-cmd", "score": 0.002355125248305291}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "moderncvcompatibility-cmd", "score": 0.7504160124360846}, {"caption": "\\phone[]{}", "snippet": "\\phone[$1]{$2}", "meta": "moderncvcompatibility-cmd", "score": 0.09602264063533228}, {"caption": "\\moderncvstyle{}", "snippet": "\\moderncvstyle{$1}", "meta": "moderncvcompatibility-cmd", "score": 0.09378844125415692}, {"caption": "\\firstname{}", "snippet": "\\firstname{$1}", "meta": "moderncvcompatibility-cmd", "score": 0.0070031590875754435}, {"caption": "\\cvline{}{}", "snippet": "\\cvline{$1}{$2}", "meta": "moderncvcompatibility-cmd", "score": 0.007378490468121007}, {"caption": "\\mobile{}", "snippet": "\\mobile{$1}", "meta": "moderncvcompatibility-cmd", "score": 0.022907406369946367}, {"caption": "\\familyname{}", "snippet": "\\familyname{$1}", "meta": "moderncvcompatibility-cmd", "score": 0.0070031590875754435}, {"caption": "\\section{}", "snippet": "\\section{$1}", "meta": "moderncvcompatibility-cmd", "score": 3.0952612541683835}], "helvet": [{"caption": "\\sfdefault", "snippet": "\\sfdefault", "meta": "helvet-cmd", "score": 0.008427383388519996}, {"caption": "\\sfdefault{}", "snippet": "\\sfdefault{$1}", "meta": "helvet-cmd", "score": 0.008427383388519996}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "helvet-cmd", "score": 0.00037306820619479756}], "placeins": [{"caption": "\\FloatBarrier", "snippet": "\\FloatBarrier", "meta": "placeins-cmd", "score": 0.015841933780270347}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "placeins-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "placeins-cmd", "score": 0.021170869458413965}], "colortbl": [{"caption": "\\rowcolor{}", "snippet": "\\rowcolor{$1}", "meta": "colortbl-cmd", "score": 0.05564476491638024}, {"caption": "\\rowcolor[]{}", "snippet": "\\rowcolor[$1]{$2}", "meta": "colortbl-cmd", "score": 0.05564476491638024}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "colortbl-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "colortbl-cmd", "score": 0.021170869458413965}, {"caption": "\\arrayrulecolor{}", "snippet": "\\arrayrulecolor{$1}", "meta": "colortbl-cmd", "score": 0.008538501902241319}, {"caption": "\\arrayrulecolor[]{}", "snippet": "\\arrayrulecolor[$1]{$2}", "meta": "colortbl-cmd", "score": 0.008538501902241319}, {"caption": "\\hline", "snippet": "\\hline", "meta": "colortbl-cmd", "score": 1.3209538327406387}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "colortbl-cmd", "score": 0.5473606021405326}, {"caption": "\\cellcolor[]{}", "snippet": "\\cellcolor[$1]{$2}", "meta": "colortbl-cmd", "score": 0.11068275858524645}, {"caption": "\\cellcolor{}", "snippet": "\\cellcolor{$1}", "meta": "colortbl-cmd", "score": 0.11068275858524645}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "colortbl-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "colortbl-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "colortbl-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "colortbl-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "colortbl-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "colortbl-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "colortbl-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "colortbl-cmd", "score": 0.018615449342361392}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "colortbl-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "colortbl-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "colortbl-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "colortbl-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "colortbl-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "colortbl-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "colortbl-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "colortbl-cmd", "score": 0.2864294797053033}], "appendix": [{"caption": "\\appendixpagename", "snippet": "\\appendixpagename", "meta": "appendix-cmd", "score": 0.0005082989114039268}, {"caption": "\\appendixpagename{}", "snippet": "\\appendixpagename{$1}", "meta": "appendix-cmd", "score": 0.0005082989114039268}, {"caption": "\\thechapter", "snippet": "\\thechapter", "meta": "appendix-cmd", "score": 0.011821300392639589}, {"caption": "\\sectionmark", "snippet": "\\sectionmark", "meta": "appendix-cmd", "score": 0.005008938879210868}, {"caption": "\\thesubsection", "snippet": "\\thesubsection", "meta": "appendix-cmd", "score": 0.004364729212023423}, {"caption": "\\appendixname", "snippet": "\\appendixname", "meta": "appendix-cmd", "score": 0.006491295958752496}, {"caption": "\\appendixname{}", "snippet": "\\appendixname{$1}", "meta": "appendix-cmd", "score": 0.006491295958752496}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "appendix-cmd", "score": 0.07503475348393239}, {"caption": "\\thesection", "snippet": "\\thesection", "meta": "appendix-cmd", "score": 0.011068945893347528}, {"caption": "\\thesection{}", "snippet": "\\thesection{$1}", "meta": "appendix-cmd", "score": 0.011068945893347528}, {"caption": "\\appendixpage", "snippet": "\\appendixpage", "meta": "appendix-cmd", "score": 0.0003193786370376004}, {"caption": "\\appendixpage{}", "snippet": "\\appendixpage{$1}", "meta": "appendix-cmd", "score": 0.0003193786370376004}, {"caption": "\\appendixtocname", "snippet": "\\appendixtocname", "meta": "appendix-cmd", "score": 0.0005082989114039268}, {"caption": "\\appendixtocname{}", "snippet": "\\appendixtocname{$1}", "meta": "appendix-cmd", "score": 0.0005082989114039268}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "appendix-cmd", "score": 0.0174633138331273}], "supertabular": [{"caption": "\\tabletail{}", "snippet": "\\tabletail{$1}", "meta": "supertabular-cmd", "score": 0.00284734590996941}, {"caption": "\\tablehead{}", "snippet": "\\tablehead{$1}", "meta": "supertabular-cmd", "score": 0.002940437317353234}, {"caption": "\\tablelasttail{}", "snippet": "\\tablelasttail{$1}", "meta": "supertabular-cmd", "score": 0.00284734590996941}, {"caption": "\\tablefirsthead{}", "snippet": "\\tablefirsthead{$1}", "meta": "supertabular-cmd", "score": 0.00284734590996941}], "makeidx": [{"caption": "\\printindex", "snippet": "\\printindex", "meta": "makeidx-cmd", "score": 0.004417016910870522}], "framed": [{"caption": "\\fbox{}", "snippet": "\\fbox{$1}", "meta": "framed-cmd", "score": 0.020865450075016792}], "layaureo": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "layaureo-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "layaureo-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "layaureo-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "layaureo-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "layaureo-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "layaureo-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "layaureo-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "layaureo-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "layaureo-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "layaureo-cmd", "score": 0.028955796305270766}, {"caption": "\\savegeometry{}", "snippet": "\\savegeometry{$1}", "meta": "layaureo-cmd", "score": 6.461638865465447e-05}, {"caption": "\\loadgeometry{}", "snippet": "\\loadgeometry{$1}", "meta": "layaureo-cmd", "score": 6.461638865465447e-05}, {"caption": "\\newgeometry{}", "snippet": "\\newgeometry{$1}", "meta": "layaureo-cmd", "score": 0.0025977479207639352}, {"caption": "\\geometry{}", "snippet": "\\geometry{$1}", "meta": "layaureo-cmd", "score": 0.046218420429973615}, {"caption": "\\csname", "snippet": "\\csname", "meta": "layaureo-cmd", "score": 0.008565354665444157}, {"caption": "\\restoregeometry", "snippet": "\\restoregeometry", "meta": "layaureo-cmd", "score": 0.0007546303842143648}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "layaureo-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "layaureo-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "layaureo-cmd", "score": 0.002958865219480927}], "keyval": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "keyval-cmd", "score": 0.00037306820619479756}], "physics": [{"caption": "\\sinh", "snippet": "\\sinh", "meta": "physics-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "physics-cmd", "score": 0.0006435164702005918}, {"caption": "\\curl{}", "snippet": "\\curl{$1}", "meta": "physics-cmd", "score": 0.001039136354388696}, {"caption": "\\curl", "snippet": "\\curl", "meta": "physics-cmd", "score": 0.001039136354388696}, {"caption": "\\dd", "snippet": "\\dd", "meta": "physics-cmd", "score": 0.0049652819784537965}, {"caption": "\\expval{}", "snippet": "\\expval{$1}", "meta": "physics-cmd", "score": 0.0006729185293892782}, {"caption": "\\exp", "snippet": "\\exp", "meta": "physics-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "physics-cmd", "score": 0.02404262443651467}, {"caption": "\\mqty", "snippet": "\\mqty", "meta": "physics-cmd", "score": 0.0002048562866401335}, {"caption": "\\order{}", "snippet": "\\order{$1}", "meta": "physics-cmd", "score": 0.00019980403788140113}, {"caption": "\\order", "snippet": "\\order", "meta": "physics-cmd", "score": 0.00019980403788140113}, {"caption": "\\abs{}", "snippet": "\\abs{$1}", "meta": "physics-cmd", "score": 0.016268920166928613}, {"caption": "\\cos", "snippet": "\\cos", "meta": "physics-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "physics-cmd", "score": 0.050370402546134785}, {"caption": "\\dv{}{}", "snippet": "\\dv{$1}{$2}", "meta": "physics-cmd", "score": 0.005139463745615663}, {"caption": "\\dv[]{}{}", "snippet": "\\dv[$1]{$2}{$3}", "meta": "physics-cmd", "score": 0.005139463745615663}, {"caption": "\\eval{}", "snippet": "\\eval{$1}", "meta": "physics-cmd", "score": 0.00021313621676565867}, {"caption": "\\eval", "snippet": "\\eval", "meta": "physics-cmd", "score": 0.00021313621676565867}, {"caption": "\\eval[]{}", "snippet": "\\eval[$1]{$2}", "meta": "physics-cmd", "score": 0.00021313621676565867}, {"caption": "\\tan", "snippet": "\\tan", "meta": "physics-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "physics-cmd", "score": 0.005640718203101287}, {"caption": "\\ket{}", "snippet": "\\ket{$1}", "meta": "physics-cmd", "score": 0.0326276280979336}, {"caption": "\\mel{}{}{}", "snippet": "\\mel{$1}{$2}{$3}", "meta": "physics-cmd", "score": 0.001123156900573353}, {"caption": "\\ip", "snippet": "\\ip", "meta": "physics-cmd", "score": 0.0008534664860896849}, {"caption": "\\ip{}{}", "snippet": "\\ip{$1}{$2}", "meta": "physics-cmd", "score": 0.0008534664860896849}, {"caption": "\\ip[]{}", "snippet": "\\ip[$1]{$2}", "meta": "physics-cmd", "score": 0.0008534664860896849}, {"caption": "\\Im", "snippet": "\\Im", "meta": "physics-cmd", "score": 0.0013451768070134808}, {"caption": "\\Im{}", "snippet": "\\Im{$1}", "meta": "physics-cmd", "score": 0.0013451768070134808}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "physics-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "physics-cmd", "score": 0.0008896391580266903}, {"caption": "\\comm{}{}", "snippet": "\\comm{$1}{$2}", "meta": "physics-cmd", "score": 0.0012026610554672049}, {"caption": "\\qty", "snippet": "\\qty", "meta": "physics-cmd", "score": 0.0017737618641299655}, {"caption": "\\qty{}", "snippet": "\\qty{$1}", "meta": "physics-cmd", "score": 0.0017737618641299655}, {"caption": "\\Tr", "snippet": "\\Tr", "meta": "physics-cmd", "score": 0.004615158124783136}, {"caption": "\\Tr{}", "snippet": "\\Tr{$1}", "meta": "physics-cmd", "score": 0.004615158124783136}, {"caption": "\\bra{}", "snippet": "\\bra{$1}", "meta": "physics-cmd", "score": 0.005609763332417241}, {"caption": "\\poissonbracket{}{}", "snippet": "\\poissonbracket{$1}{$2}", "meta": "physics-cmd", "score": 2.2761809626681494e-05}, {"caption": "\\pmat{}", "snippet": "\\pmat{$1}", "meta": "physics-cmd", "score": 0.00010356789132354732}, {"caption": "\\norm{}", "snippet": "\\norm{$1}", "meta": "physics-cmd", "score": 0.006576610603906938}, {"caption": "\\cot", "snippet": "\\cot", "meta": "physics-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "physics-cmd", "score": 0.0003640644365701238}, {"caption": "\\cross", "snippet": "\\cross", "meta": "physics-cmd", "score": 0.0005412940211650938}, {"caption": "\\log", "snippet": "\\log", "meta": "physics-cmd", "score": 0.048131780413380156}, {"caption": "\\dmat{}", "snippet": "\\dmat{$1}", "meta": "physics-cmd", "score": 2.2761809626681494e-05}, {"caption": "\\Re", "snippet": "\\Re", "meta": "physics-cmd", "score": 0.0031525922563281736}, {"caption": "\\Re{}", "snippet": "\\Re{$1}", "meta": "physics-cmd", "score": 0.0031525922563281736}, {"caption": "\\qq{}", "snippet": "\\qq{$1}", "meta": "physics-cmd", "score": 8.241282620919185e-05}, {"caption": "\\qq", "snippet": "\\qq", "meta": "physics-cmd", "score": 8.241282620919185e-05}, {"caption": "\\vb{}", "snippet": "\\vb{$1}", "meta": "physics-cmd", "score": 0.007377410801695042}, {"caption": "\\pdv{}{}", "snippet": "\\pdv{$1}{$2}", "meta": "physics-cmd", "score": 0.0014087913646471247}, {"caption": "\\pdv{}{}{}", "snippet": "\\pdv{$1}{$2}{$3}", "meta": "physics-cmd", "score": 0.0014087913646471247}, {"caption": "\\braket{}{}", "snippet": "\\braket{$1}{$2}", "meta": "physics-cmd", "score": 0.004421747491186916}, {"caption": "\\braket{}", "snippet": "\\braket{$1}", "meta": "physics-cmd", "score": 0.004421747491186916}, {"caption": "\\div", "snippet": "\\div", "meta": "physics-cmd", "score": 0.002403050103349905}, {"caption": "\\div{}", "snippet": "\\div{$1}", "meta": "physics-cmd", "score": 0.002403050103349905}, {"caption": "\\sin", "snippet": "\\sin", "meta": "physics-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "physics-cmd", "score": 0.040463088537699636}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "physics-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "physics-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "physics-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "physics-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "physics-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "physics-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "physics-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "physics-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "physics-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "physics-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "physics-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "physics-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "physics-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "physics-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "physics-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "physics-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "physics-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "physics-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "physics-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "physics-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "physics-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "physics-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "physics-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "physics-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "physics-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "physics-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "physics-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "physics-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "physics-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "physics-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "physics-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "physics-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "physics-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "physics-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "physics-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "physics-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "physics-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "physics-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "physics-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "physics-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "physics-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "physics-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "physics-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "physics-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "physics-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "physics-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "physics-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "physics-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "physics-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "physics-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "physics-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "physics-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "physics-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "physics-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "physics-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "physics-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "physics-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "physics-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "physics-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "physics-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "physics-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "physics-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "physics-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "physics-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "physics-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "physics-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "physics-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "physics-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "physics-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "physics-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "physics-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "physics-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "physics-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "physics-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "physics-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "physics-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "physics-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "physics-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "physics-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "physics-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "physics-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "physics-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "physics-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "physics-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "physics-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "physics-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "physics-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "physics-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "physics-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "physics-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "physics-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "physics-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "physics-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "physics-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "physics-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "physics-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "physics-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "physics-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "physics-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "physics-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "physics-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "physics-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "physics-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "physics-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "physics-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "physics-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "physics-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "physics-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "physics-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "physics-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "physics-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "physics-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "physics-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "physics-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "physics-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "physics-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "physics-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "physics-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "physics-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "physics-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "physics-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "physics-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "physics-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "physics-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "physics-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "physics-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "physics-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "physics-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "physics-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "physics-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "physics-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "physics-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "physics-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "physics-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "physics-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "physics-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "physics-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "physics-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "physics-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "physics-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "physics-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "physics-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "physics-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "physics-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "physics-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "physics-cmd", "score": 0.0004286136584068833}, {"caption": "\\do", "snippet": "\\do", "meta": "physics-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "physics-cmd", "score": 0.0063276692758974925}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "physics-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "physics-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "physics-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "physics-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "physics-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "physics-cmd", "score": 0.2864294797053033}], "authblk": [{"caption": "\\Authfont{}", "snippet": "\\Authfont{$1}", "meta": "authblk-cmd", "score": 0.00019538157043798684}, {"caption": "\\thanks{}", "snippet": "\\thanks{$1}", "meta": "authblk-cmd", "score": 0.08382259880654083}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "authblk-cmd", "score": 0.7504160124360846}, {"caption": "\\rlap{}", "snippet": "\\rlap{$1}", "meta": "authblk-cmd", "score": 0.01269300721396509}, {"caption": "\\Authands{}", "snippet": "\\Authands{$1}", "meta": "authblk-cmd", "score": 0.00043932814970131613}, {"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "authblk-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "authblk-cmd", "score": 0.8973590434087177}, {"caption": "\\textsuperscript{}", "snippet": "\\textsuperscript{$1}", "meta": "authblk-cmd", "score": 0.05216393882408519}, {"caption": "\\Affilfont{}", "snippet": "\\Affilfont{$1}", "meta": "authblk-cmd", "score": 0.0004505484831792931}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "authblk-cmd", "score": 0.2253056071787701}, {"caption": "\\affil[]{}", "snippet": "\\affil[$1]{$2}", "meta": "authblk-cmd", "score": 0.014174618039587864}, {"caption": "\\affil{}", "snippet": "\\affil{$1}", "meta": "authblk-cmd", "score": 0.014174618039587864}], "tabu": [{"caption": "\\extrarowheight", "snippet": "\\extrarowheight", "meta": "tabu-cmd", "score": 0.003735645243417412}, {"caption": "\\extrarowheight{}", "snippet": "\\extrarowheight{$1}", "meta": "tabu-cmd", "score": 0.003735645243417412}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "tabu-cmd", "score": 0.5473606021405326}, {"caption": "\\do", "snippet": "\\do", "meta": "tabu-cmd", "score": 0.009278344180101056}, {"caption": "\\hfill", "snippet": "\\hfill", "meta": "tabu-cmd", "score": 0.2058248088519886}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "tabu-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "tabu-cmd", "score": 0.022224283488673075}, {"caption": "\\tabulinesep", "snippet": "\\tabulinesep", "meta": "tabu-cmd", "score": 0.0008256968285249214}, {"caption": "\\hskip", "snippet": "\\hskip", "meta": "tabu-cmd", "score": 0.04339822811565144}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "tabu-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "tabu-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "tabu-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "tabu-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "tabu-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tabu-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "tabu-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "tabu-cmd", "score": 0.018615449342361392}, {"caption": "\\par", "snippet": "\\par", "meta": "tabu-cmd", "score": 0.413853376001159}], "CJKutf8": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "CJKutf8-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "CJKutf8-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "CJKutf8-cmd", "score": 0.021170869458413965}, {"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "CJKutf8-cmd", "score": 0.04598628699063736}, {"caption": "\\inputencoding{}", "snippet": "\\inputencoding{$1}", "meta": "CJKutf8-cmd", "score": 0.0002447047447770061}], "sectsty": [{"caption": "\\chapterfont{}", "snippet": "\\chapterfont{$1}", "meta": "sectsty-cmd", "score": 0.0001572081344977262}, {"caption": "\\raggedright", "snippet": "\\raggedright", "meta": "sectsty-cmd", "score": 0.05314494127699766}, {"caption": "\\sectionfont{}", "snippet": "\\sectionfont{$1}", "meta": "sectsty-cmd", "score": 0.003867941482301249}, {"caption": "\\paragraph{}", "snippet": "\\paragraph{$1}", "meta": "sectsty-cmd", "score": 0.152074250347974}, {"caption": "\\allsectionsfont{}", "snippet": "\\allsectionsfont{$1}", "meta": "sectsty-cmd", "score": 0.0011367198619746117}, {"caption": "\\subsection{}", "snippet": "\\subsection{$1}", "meta": "sectsty-cmd", "score": 1.3890912739512353}, {"caption": "\\subsectionfont{}", "snippet": "\\subsectionfont{$1}", "meta": "sectsty-cmd", "score": 0.002811633808315226}, {"caption": "\\interlinepenalty", "snippet": "\\interlinepenalty", "meta": "sectsty-cmd", "score": 0.00032069955588347133}, {"caption": "\\subsubsectionfont{}", "snippet": "\\subsubsectionfont{$1}", "meta": "sectsty-cmd", "score": 0.0011363939259266408}, {"caption": "\\underline{}", "snippet": "\\underline{$1}", "meta": "sectsty-cmd", "score": 0.14748550887002482}, {"caption": "\\subsubsection{}", "snippet": "\\subsubsection{$1}", "meta": "sectsty-cmd", "score": 0.3727781330132016}, {"caption": "\\section{}", "snippet": "\\section{$1}", "meta": "sectsty-cmd", "score": 3.0952612541683835}], "lscape": [{"caption": "\\csname", "snippet": "\\csname", "meta": "lscape-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "lscape-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "lscape-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "lscape-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "lscape-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "lscape-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "lscape-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "lscape-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "lscape-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "lscape-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "lscape-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "lscape-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "lscape-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "lscape-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "lscape-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "lscape-cmd", "score": 0.004649150613625593}], "hyphenat": [{"caption": "\\hyp{}", "snippet": "\\hyp{$1}", "meta": "hyphenat-cmd", "score": 0.0013359874951570454}], "tocloft": [{"caption": "\\cftsecleader", "snippet": "\\cftsecleader", "meta": "tocloft-cmd", "score": 0.0011340882025681251}, {"caption": "\\cftloftitlefont", "snippet": "\\cftloftitlefont", "meta": "tocloft-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\cftchappresnum{}", "snippet": "\\cftchappresnum{$1}", "meta": "tocloft-cmd", "score": 2.8671864736205568e-05}, {"caption": "\\cftchappresnum", "snippet": "\\cftchappresnum", "meta": "tocloft-cmd", "score": 2.8671864736205568e-05}, {"caption": "\\listoftables", "snippet": "\\listoftables", "meta": "tocloft-cmd", "score": 0.02104656820469027}, {"caption": "\\cftsecfont{}", "snippet": "\\cftsecfont{$1}", "meta": "tocloft-cmd", "score": 5.630015640183448e-05}, {"caption": "\\cftchapfont{}", "snippet": "\\cftchapfont{$1}", "meta": "tocloft-cmd", "score": 6.253521408609416e-05}, {"caption": "\\cftchapfont", "snippet": "\\cftchapfont", "meta": "tocloft-cmd", "score": 6.253521408609416e-05}, {"caption": "\\cftsubsecleader", "snippet": "\\cftsubsecleader", "meta": "tocloft-cmd", "score": 1.0644172549700836e-05}, {"caption": "\\cftchapleader", "snippet": "\\cftchapleader", "meta": "tocloft-cmd", "score": 1.0644172549700836e-05}, {"caption": "\\tocloftpagestyle{}", "snippet": "\\tocloftpagestyle{$1}", "meta": "tocloft-cmd", "score": 8.392451158032374e-05}, {"caption": "\\cfttoctitlefont", "snippet": "\\cfttoctitlefont", "meta": "tocloft-cmd", "score": 6.877027177035383e-05}, {"caption": "\\cftdot", "snippet": "\\cftdot", "meta": "tocloft-cmd", "score": 1.6201749367686227e-05}, {"caption": "\\cftsecdotsep", "snippet": "\\cftsecdotsep", "meta": "tocloft-cmd", "score": 0.0029383990986223767}, {"caption": "\\cftafterloftitle", "snippet": "\\cftafterloftitle", "meta": "tocloft-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\listoffigures", "snippet": "\\listoffigures", "meta": "tocloft-cmd", "score": 0.03447318897846567}, {"caption": "\\cftdotfill{}", "snippet": "\\cftdotfill{$1}", "meta": "tocloft-cmd", "score": 0.006027562229085753}, {"caption": "\\tableofcontents", "snippet": "\\tableofcontents", "meta": "tocloft-cmd", "score": 0.13360595130994957}, {"caption": "\\cftdotsep", "snippet": "\\cftdotsep", "meta": "tocloft-cmd", "score": 0.003089163130463376}, {"caption": "\\numberline{}", "snippet": "\\numberline{$1}", "meta": "tocloft-cmd", "score": 0.007461440567272885}, {"caption": "\\cftlottitlefont", "snippet": "\\cftlottitlefont", "meta": "tocloft-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\cftchappagefont{}", "snippet": "\\cftchappagefont{$1}", "meta": "tocloft-cmd", "score": 5.630015640183448e-05}, {"caption": "\\cftsetindents{}{}{}", "snippet": "\\cftsetindents{$1}{$2}{$3}", "meta": "tocloft-cmd", "score": 0.00043647269161217853}, {"caption": "\\cftsecpagefont{}", "snippet": "\\cftsecpagefont{$1}", "meta": "tocloft-cmd", "score": 5.630015640183448e-05}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "tocloft-cmd", "score": 0.0174633138331273}, {"caption": "\\cftaftertoctitle", "snippet": "\\cftaftertoctitle", "meta": "tocloft-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\cftafterlottitle", "snippet": "\\cftafterlottitle", "meta": "tocloft-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\newlistof{}{}{}", "snippet": "\\newlistof{$1}{$2}{$3}", "meta": "tocloft-cmd", "score": 0.0005381264966408724}], "glossaries": [{"caption": "\\glslongpluralkey", "snippet": "\\glslongpluralkey", "meta": "glossaries-cmd", "score": 1.4538687447297259e-05}, {"caption": "\\Glspl{}", "snippet": "\\Glspl{$1}", "meta": "glossaries-cmd", "score": 0.0025291265119320736}, {"caption": "\\glossarysection", "snippet": "\\glossarysection", "meta": "glossaries-cmd", "score": 9.579755294730752e-05}, {"caption": "\\printglossaries", "snippet": "\\printglossaries", "meta": "glossaries-cmd", "score": 0.0010106582768889887}, {"caption": "\\Gls{}", "snippet": "\\Gls{$1}", "meta": "glossaries-cmd", "score": 0.003696678698317109}, {"caption": "\\setglossarystyle{}", "snippet": "\\setglossarystyle{$1}", "meta": "glossaries-cmd", "score": 0.0003758893277679221}, {"caption": "\\printglossary", "snippet": "\\printglossary", "meta": "glossaries-cmd", "score": 0.009139682306158714}, {"caption": "\\printglossary[]", "snippet": "\\printglossary[$1]", "meta": "glossaries-cmd", "score": 0.009139682306158714}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-cmd", "score": 0.009278344180101056}, {"caption": "\\setglossarysection{}", "snippet": "\\setglossarysection{$1}", "meta": "glossaries-cmd", "score": 3.6081414102781514e-05}, {"caption": "\\glsresetall", "snippet": "\\glsresetall", "meta": "glossaries-cmd", "score": 0.0006123462672467326}, {"caption": "\\the", "snippet": "\\the", "meta": "glossaries-cmd", "score": 0.007238960303946444}, {"caption": "\\acrshort{}", "snippet": "\\acrshort{$1}", "meta": "glossaries-cmd", "score": 0.009936841864059727}, {"caption": "\\printnoidxglossary[]", "snippet": "\\printnoidxglossary[$1]", "meta": "glossaries-cmd", "score": 0.00021912375285685037}, {"caption": "\\newglossary{}{}", "snippet": "\\newglossary{$1}{$2}", "meta": "glossaries-cmd", "score": 1.4547244650032571e-05}, {"caption": "\\gls{}", "snippet": "\\gls{$1}", "meta": "glossaries-cmd", "score": 0.06939353309055077}, {"caption": "\\printnoidxglossaries", "snippet": "\\printnoidxglossaries", "meta": "glossaries-cmd", "score": 5.6789564226023136e-05}, {"caption": "\\printindex", "snippet": "\\printindex", "meta": "glossaries-cmd", "score": 0.004417016910870522}, {"caption": "\\defglsentryfmt[]{}", "snippet": "\\defglsentryfmt[$1]{$2}", "meta": "glossaries-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\glspostdescription", "snippet": "\\glspostdescription", "meta": "glossaries-cmd", "score": 0.0006337376579591112}, {"caption": "\\number", "snippet": "\\number", "meta": "glossaries-cmd", "score": 0.000968714260809983}, {"caption": "\\glsaddall", "snippet": "\\glsaddall", "meta": "glossaries-cmd", "score": 0.0008363820557740373}, {"caption": "\\glsaddall[]", "snippet": "\\glsaddall[$1]", "meta": "glossaries-cmd", "score": 0.0008363820557740373}, {"caption": "\\makeglossaries", "snippet": "\\makeglossaries", "meta": "glossaries-cmd", "score": 0.0056737600836936995}, {"caption": "\\glossaryname", "snippet": "\\glossaryname", "meta": "glossaries-cmd", "score": 0.0006174536302752427}, {"caption": "\\newglossaryentry{}{}", "snippet": "\\newglossaryentry{$1}{$2}", "meta": "glossaries-cmd", "score": 0.018524394136900962}, {"caption": "\\glslabel", "snippet": "\\glslabel", "meta": "glossaries-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\glsadd{}", "snippet": "\\glsadd{$1}", "meta": "glossaries-cmd", "score": 3.0150373480213892e-05}, {"caption": "\\makenoidxglossaries", "snippet": "\\makenoidxglossaries", "meta": "glossaries-cmd", "score": 0.0001382210125680805}, {"caption": "\\glsgenentryfmt", "snippet": "\\glsgenentryfmt", "meta": "glossaries-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\acronymtype", "snippet": "\\acronymtype", "meta": "glossaries-cmd", "score": 0.002000834271117562}, {"caption": "\\acrfull{}", "snippet": "\\acrfull{$1}", "meta": "glossaries-cmd", "score": 0.0032622587277765067}, {"caption": "\\newacronym{}{}{}", "snippet": "\\newacronym{$1}{$2}{$3}", "meta": "glossaries-cmd", "score": 0.03193935544723102}, {"caption": "\\glspl{}", "snippet": "\\glspl{$1}", "meta": "glossaries-cmd", "score": 0.0034025897522047717}, {"caption": "\\ifglsused{}{}{}", "snippet": "\\ifglsused{$1}{$2}{$3}", "meta": "glossaries-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\acrlong{}", "snippet": "\\acrlong{$1}", "meta": "glossaries-cmd", "score": 0.002517821598213752}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "glossaries-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "glossaries-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "glossaries-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "glossaries-cmd", "score": 0.021170869458413965}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "glossaries-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "glossaries-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "glossaries-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "glossaries-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "glossaries-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "glossaries-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "glossaries-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "glossaries-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "glossaries-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "glossaries-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "glossaries-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "glossaries-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "glossaries-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "glossaries-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "glossaries-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "glossaries-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "glossaries-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "glossaries-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "glossaries-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "glossaries-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "glossaries-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "glossaries-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "glossaries-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "glossaries-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "glossaries-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "glossaries-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "glossaries-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "glossaries-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "glossaries-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "glossaries-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "glossaries-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "glossaries-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "glossaries-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "glossaries-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "glossaries-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "glossaries-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "glossaries-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "glossaries-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "glossaries-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "glossaries-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "glossaries-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "glossaries-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "glossaries-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "glossaries-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "glossaries-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "glossaries-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "glossaries-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "glossaries-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "glossaries-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "glossaries-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "glossaries-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "glossaries-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "glossaries-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "glossaries-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "glossaries-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "glossaries-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "glossaries-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "glossaries-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "glossaries-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "glossaries-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "glossaries-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "glossaries-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "glossaries-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "glossaries-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "glossaries-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "glossaries-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "glossaries-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "glossaries-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "glossaries-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "glossaries-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "glossaries-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "glossaries-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "glossaries-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "glossaries-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "glossaries-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "glossaries-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "glossaries-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "glossaries-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "glossaries-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "glossaries-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "glossaries-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "glossaries-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "glossaries-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "glossaries-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "glossaries-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "glossaries-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "glossaries-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "glossaries-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "glossaries-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "glossaries-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "glossaries-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "glossaries-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "glossaries-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "glossaries-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "glossaries-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "glossaries-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "glossaries-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "glossaries-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "glossaries-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "glossaries-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "glossaries-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "glossaries-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "glossaries-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "glossaries-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "glossaries-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "glossaries-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "glossaries-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "glossaries-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "glossaries-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "glossaries-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "glossaries-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "glossaries-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "glossaries-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "glossaries-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "glossaries-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "glossaries-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "glossaries-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "glossaries-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "glossaries-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "glossaries-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "glossaries-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "glossaries-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "glossaries-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "glossaries-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "glossaries-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "glossaries-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "glossaries-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "glossaries-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "glossaries-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "glossaries-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "glossaries-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "glossaries-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "glossaries-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "glossaries-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "glossaries-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "glossaries-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "glossaries-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "glossaries-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "glossaries-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "glossaries-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "glossaries-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "glossaries-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "glossaries-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "glossaries-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "glossaries-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "glossaries-cmd", "score": 0.008565354665444157}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "glossaries-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "glossaries-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "glossaries-cmd", "score": 0.18137737738638837}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "glossaries-cmd", "score": 2.341195220791228}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "glossaries-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "glossaries-cmd", "score": 0.021170869458413965}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "glossaries-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "glossaries-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "glossaries-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "glossaries-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "glossaries-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "glossaries-cmd", "score": 0.0018957469739775527}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "glossaries-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "glossaries-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "glossaries-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "glossaries-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "glossaries-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "glossaries-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "glossaries-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "glossaries-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "glossaries-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "glossaries-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "glossaries-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "glossaries-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "glossaries-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "glossaries-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "glossaries-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "glossaries-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "glossaries-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "glossaries-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "glossaries-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "glossaries-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "glossaries-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "glossaries-cmd", "score": 0.0063276692758974925}], "cleveref": [{"caption": "\\crefdefaultlabelformat{}", "snippet": "\\crefdefaultlabelformat{$1}", "meta": "cleveref-cmd", "score": 8.401009062000455e-06}, {"caption": "\\crefname{}{}{}", "snippet": "\\crefname{$1}{$2}{$3}", "meta": "cleveref-cmd", "score": 0.0016963440482621792}, {"caption": "\\crefrangeformat{}{}", "snippet": "\\crefrangeformat{$1}{$2}", "meta": "cleveref-cmd", "score": 0.00021116765384691477}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "cleveref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "cleveref-cmd", "score": 0.021170869458413965}, {"caption": "\\crefmultiformat{}{}", "snippet": "\\crefmultiformat{$1}{$2}", "meta": "cleveref-cmd", "score": 0.00021116765384691477}, {"caption": "\\crefformat{}{}", "snippet": "\\crefformat{$1}{$2}", "meta": "cleveref-cmd", "score": 0.0006776840671975755}, {"caption": "\\Cref{}", "snippet": "\\Cref{$1}", "meta": "cleveref-cmd", "score": 0.0016649686371949341}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "cleveref-cmd", "score": 0.002140559856649122}, {"caption": "\\cref{}", "snippet": "\\cref{$1}", "meta": "cleveref-cmd", "score": 0.0159491058092361}, {"caption": "\\crefrangeconjunction", "snippet": "\\crefrangeconjunction", "meta": "cleveref-cmd", "score": 3.2405622997778076e-06}, {"caption": "\\csname", "snippet": "\\csname", "meta": "cleveref-cmd", "score": 0.008565354665444157}, {"caption": "\\creflabelformat{}{}", "snippet": "\\creflabelformat{$1}{$2}", "meta": "cleveref-cmd", "score": 0.000997031755478214}, {"caption": "\\Crefname{}{}{}", "snippet": "\\Crefname{$1}{$2}{$3}", "meta": "cleveref-cmd", "score": 0.000239288793927364}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "cleveref-cmd", "score": 1.897791904799601}, {"caption": "\\labelcref{}", "snippet": "\\labelcref{$1}", "meta": "cleveref-cmd", "score": 6.720807249600364e-05}, {"caption": "\\creflastconjunction", "snippet": "\\creflastconjunction", "meta": "cleveref-cmd", "score": 3.2405622997778076e-06}], "eso-pic": [{"caption": "\\AddToShipoutPictureFG{}", "snippet": "\\AddToShipoutPictureFG{$1}", "meta": "eso-pic-cmd", "score": 0.000325977535138643}, {"caption": "\\AddToShipoutPictureBG{}", "snippet": "\\AddToShipoutPictureBG{$1}", "meta": "eso-pic-cmd", "score": 0.0008957666085644653}, {"caption": "\\AtPageUpperLeft{}", "snippet": "\\AtPageUpperLeft{$1}", "meta": "eso-pic-cmd", "score": 0.0003608141410278152}, {"caption": "\\LenToUnit{}", "snippet": "\\LenToUnit{$1}", "meta": "eso-pic-cmd", "score": 0.0007216282820556304}, {"caption": "\\AddToShipoutPicture{}", "snippet": "\\AddToShipoutPicture{$1}", "meta": "eso-pic-cmd", "score": 0.0017658629469099734}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "eso-pic-cmd", "score": 0.00037306820619479756}, {"caption": "\\empty", "snippet": "\\empty", "meta": "eso-pic-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "eso-pic-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "eso-pic-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "eso-pic-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "eso-pic-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "eso-pic-cmd", "score": 0.008565354665444157}], "mhchem": [{"caption": "\\ce{}", "snippet": "\\ce{$1}", "meta": "mhchem-cmd", "score": 0.04246600383063094}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mhchem-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mhchem-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "mhchem-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "mhchem-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "mhchem-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "mhchem-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "mhchem-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mhchem-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "mhchem-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mhchem-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "mhchem-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "mhchem-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "mhchem-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "mhchem-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "mhchem-cmd", "score": 0.004649150613625593}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "mhchem-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "mhchem-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "mhchem-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "mhchem-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "mhchem-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "mhchem-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "mhchem-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "mhchem-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "mhchem-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "mhchem-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "mhchem-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "mhchem-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "mhchem-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "mhchem-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "mhchem-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "mhchem-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "mhchem-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "mhchem-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "mhchem-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "mhchem-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "mhchem-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "mhchem-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "mhchem-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "mhchem-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "mhchem-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "mhchem-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "mhchem-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "mhchem-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "mhchem-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "mhchem-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "mhchem-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "mhchem-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "mhchem-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "mhchem-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "mhchem-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "mhchem-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "mhchem-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "mhchem-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "mhchem-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "mhchem-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "mhchem-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "mhchem-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "mhchem-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "mhchem-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "mhchem-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "mhchem-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "mhchem-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "mhchem-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "mhchem-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "mhchem-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "mhchem-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "mhchem-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "mhchem-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "mhchem-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "mhchem-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "mhchem-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "mhchem-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "mhchem-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "mhchem-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "mhchem-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "mhchem-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "mhchem-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "mhchem-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "mhchem-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "mhchem-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "mhchem-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "mhchem-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "mhchem-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "mhchem-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "mhchem-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "mhchem-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "mhchem-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "mhchem-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "mhchem-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "mhchem-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "mhchem-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "mhchem-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "mhchem-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "mhchem-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "mhchem-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "mhchem-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "mhchem-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "mhchem-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "mhchem-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "mhchem-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "mhchem-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "mhchem-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "mhchem-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "mhchem-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "mhchem-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "mhchem-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "mhchem-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "mhchem-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "mhchem-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "mhchem-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "mhchem-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "mhchem-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "mhchem-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "mhchem-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "mhchem-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "mhchem-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "mhchem-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "mhchem-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "mhchem-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "mhchem-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "mhchem-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "mhchem-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "mhchem-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "mhchem-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "mhchem-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "mhchem-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "mhchem-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "mhchem-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "mhchem-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "mhchem-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "mhchem-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "mhchem-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "mhchem-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "mhchem-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "mhchem-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "mhchem-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "mhchem-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "mhchem-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "mhchem-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "mhchem-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "mhchem-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "mhchem-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "mhchem-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "mhchem-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "mhchem-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "mhchem-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "mhchem-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "mhchem-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "mhchem-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "mhchem-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "mhchem-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "mhchem-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "mhchem-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "mhchem-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "mhchem-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "mhchem-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "mhchem-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "mhchem-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "mhchem-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "mhchem-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "mhchem-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mhchem-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "mhchem-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "mhchem-cmd", "score": 0.2864294797053033}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "mhchem-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "mhchem-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "mhchem-cmd", "score": 0.18137737738638837}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "mhchem-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "mhchem-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "mhchem-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mhchem-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mhchem-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "mhchem-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "mhchem-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "mhchem-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "mhchem-cmd", "score": 0.028955796305270766}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mhchem-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "mhchem-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "mhchem-cmd", "score": 0.0063276692758974925}], "amscd": [{"caption": "\\tag{}", "snippet": "\\tag{$1}", "meta": "amscd-cmd", "score": 0.00784357461002059}, {"caption": "\\do", "snippet": "\\do", "meta": "amscd-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "amscd-cmd", "score": 0.0063276692758974925}], "unicode-math": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "unicode-math-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "unicode-math-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "unicode-math-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "unicode-math-cmd", "score": 0.2864294797053033}], "ifxetex": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "ifxetex-cmd", "score": 0.00021116765384691477}], "newtxmath": [{"caption": "\\int", "snippet": "\\int", "meta": "newtxmath-cmd", "score": 0.11946660537765894}, {"caption": "\\sqrt{}", "snippet": "\\sqrt{$1}", "meta": "newtxmath-cmd", "score": 0.20240160977404634}, {"caption": "\\prod", "snippet": "\\prod", "meta": "newtxmath-cmd", "score": 0.02549889375975901}, {"caption": "\\hbar", "snippet": "\\hbar", "meta": "newtxmath-cmd", "score": 0.024733493787737763}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "newtxmath-cmd", "score": 0.006473769486518971}, {"caption": "\\surd", "snippet": "\\surd", "meta": "newtxmath-cmd", "score": 0.002159694087964359}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "newtxmath-cmd", "score": 0.0058847868741168765}, {"caption": "\\sum", "snippet": "\\sum", "meta": "newtxmath-cmd", "score": 0.42607994509619934}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "newtxmath-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "newtxmath-cmd", "score": 0.015507614799858266}, {"caption": "\\vdots", "snippet": "\\vdots", "meta": "newtxmath-cmd", "score": 0.03669355896719803}, {"caption": "\\ddots", "snippet": "\\ddots", "meta": "newtxmath-cmd", "score": 0.010831382784078964}, {"caption": "\\csname", "snippet": "\\csname", "meta": "newtxmath-cmd", "score": 0.008565354665444157}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "newtxmath-cmd", "score": 0.04318078602869565}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "newtxmath-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "newtxmath-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "newtxmath-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "newtxmath-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "newtxmath-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "newtxmath-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "newtxmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "newtxmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "newtxmath-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "newtxmath-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "newtxmath-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "newtxmath-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "newtxmath-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "newtxmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "newtxmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "newtxmath-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "newtxmath-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "newtxmath-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "newtxmath-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "newtxmath-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "newtxmath-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "newtxmath-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "newtxmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "newtxmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "newtxmath-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "newtxmath-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "newtxmath-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "newtxmath-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "newtxmath-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "newtxmath-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "newtxmath-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "newtxmath-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "newtxmath-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "newtxmath-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "newtxmath-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "newtxmath-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "newtxmath-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "newtxmath-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "newtxmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "newtxmath-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "newtxmath-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "newtxmath-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "newtxmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "newtxmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "newtxmath-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "newtxmath-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "newtxmath-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "newtxmath-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "newtxmath-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "newtxmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "newtxmath-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "newtxmath-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "newtxmath-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "newtxmath-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "newtxmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "newtxmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "newtxmath-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "newtxmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "newtxmath-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "newtxmath-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "newtxmath-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "newtxmath-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "newtxmath-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "newtxmath-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "newtxmath-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "newtxmath-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "newtxmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "newtxmath-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "newtxmath-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "newtxmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "newtxmath-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "newtxmath-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "newtxmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "newtxmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "newtxmath-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "newtxmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "newtxmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "newtxmath-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "newtxmath-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "newtxmath-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "newtxmath-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "newtxmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "newtxmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "newtxmath-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "newtxmath-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "newtxmath-cmd", "score": 0.0058847868741168765}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "newtxmath-cmd", "score": 0.00021116765384691477}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "newtxmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "newtxmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "newtxmath-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "newtxmath-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "newtxmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "newtxmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "newtxmath-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "newtxmath-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "newtxmath-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "newtxmath-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "newtxmath-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "newtxmath-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "newtxmath-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "newtxmath-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "newtxmath-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "newtxmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "newtxmath-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "newtxmath-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "newtxmath-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "newtxmath-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "newtxmath-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "newtxmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "newtxmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "newtxmath-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "newtxmath-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "newtxmath-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "newtxmath-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "newtxmath-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "newtxmath-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "newtxmath-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "newtxmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "newtxmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "newtxmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "newtxmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "newtxmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "newtxmath-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "newtxmath-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "newtxmath-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "newtxmath-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "newtxmath-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "newtxmath-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "newtxmath-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "newtxmath-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "newtxmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "newtxmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "newtxmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "newtxmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "newtxmath-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "newtxmath-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "newtxmath-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "newtxmath-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "newtxmath-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "newtxmath-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "newtxmath-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "newtxmath-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "newtxmath-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "newtxmath-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "newtxmath-cmd", "score": 0.0063276692758974925}], "pdflscape": [{"caption": "\\csname", "snippet": "\\csname", "meta": "pdflscape-cmd", "score": 0.008565354665444157}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "pdflscape-cmd", "score": 0.00021116765384691477}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pdflscape-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pdflscape-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pdflscape-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pdflscape-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pdflscape-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pdflscape-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pdflscape-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pdflscape-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pdflscape-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pdflscape-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pdflscape-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pdflscape-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pdflscape-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pdflscape-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pdflscape-cmd", "score": 0.004649150613625593}], "apacite": [{"caption": "\\citep{}", "snippet": "\\citep{$1}", "meta": "apacite-cmd", "score": 0.2941882834697057}, {"caption": "\\citet{}", "snippet": "\\citet{$1}", "meta": "apacite-cmd", "score": 0.09046048561361801}, {"caption": "\\url{}", "snippet": "\\url{$1}", "meta": "apacite-cmd", "score": 0.13586474005868793}, {"caption": "\\BPGS", "snippet": "\\BPGS", "meta": "apacite-cmd", "score": 0.00023651453263545777}, {"caption": "\\shortcite{}", "snippet": "\\shortcite{$1}", "meta": "apacite-cmd", "score": 0.010082057767216608}, {"caption": "\\shortciteA{}", "snippet": "\\shortciteA{$1}", "meta": "apacite-cmd", "score": 0.0011019769466422762}, {"caption": "\\nocite{}", "snippet": "\\nocite{$1}", "meta": "apacite-cmd", "score": 0.04990693820960752}, {"caption": "\\refname", "snippet": "\\refname", "meta": "apacite-cmd", "score": 0.006490238196722249}, {"caption": "\\refname{}", "snippet": "\\refname{$1}", "meta": "apacite-cmd", "score": 0.006490238196722249}, {"caption": "\\citeA{}", "snippet": "\\citeA{$1}", "meta": "apacite-cmd", "score": 0.008470555729707068}, {"caption": "\\citeyear{}", "snippet": "\\citeyear{$1}", "meta": "apacite-cmd", "score": 0.01091041305836494}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "apacite-cmd", "score": 2.341195220791228}, {"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "apacite-cmd", "score": 0.2659628337907604}, {"caption": "\\BPG", "snippet": "\\BPG", "meta": "apacite-cmd", "score": 0.00023651453263545777}, {"caption": "\\citeNP{}", "snippet": "\\citeNP{$1}", "meta": "apacite-cmd", "score": 0.0003168688289795556}, {"caption": "\\citeauthor{}", "snippet": "\\citeauthor{$1}", "meta": "apacite-cmd", "score": 0.01359248786373484}], "mathpazo": [{"caption": "\\big", "snippet": "\\big", "meta": "mathpazo-cmd", "score": 0.05613164277964739}, {"caption": "\\mathbb{}", "snippet": "\\mathbb{$1}", "meta": "mathpazo-cmd", "score": 0.33740449739178857}, {"caption": "\\Big", "snippet": "\\Big", "meta": "mathpazo-cmd", "score": 0.050370758781422345}], "footmisc": [{"caption": "\\footref{}", "snippet": "\\footref{$1}", "meta": "footmisc-cmd", "score": 0.0003680857021151614}, {"caption": "\\footref", "snippet": "\\footref", "meta": "footmisc-cmd", "score": 0.0003680857021151614}, {"caption": "\\protect", "snippet": "\\protect", "meta": "footmisc-cmd", "score": 0.0200686676229443}, {"caption": "\\multfootsep", "snippet": "\\multfootsep", "meta": "footmisc-cmd", "score": 0.00010171098214158578}, {"caption": "\\footnotelayout", "snippet": "\\footnotelayout", "meta": "footmisc-cmd", "score": 0.0004535003423927585}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "footmisc-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "footmisc-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "footmisc-cmd", "score": 0.021473212893597875}, {"caption": "\\thefootnote", "snippet": "\\thefootnote", "meta": "footmisc-cmd", "score": 0.007676927812687567}, {"caption": "\\thefootnote{}", "snippet": "\\thefootnote{$1}", "meta": "footmisc-cmd", "score": 0.007676927812687567}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "footmisc-cmd", "score": 0.1789117552185788}], "fixltx2e": [{"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "fixltx2e-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "fixltx2e-cmd", "score": 0.354445763583904}, {"caption": "\\textsubscript{}", "snippet": "\\textsubscript{$1}", "meta": "fixltx2e-cmd", "score": 0.058405875394131175}, {"caption": "\\em", "snippet": "\\em", "meta": "fixltx2e-cmd", "score": 0.10357353994640862}], "sidecap": [{"caption": "\\csname", "snippet": "\\csname", "meta": "sidecap-cmd", "score": 0.008565354665444157}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "sidecap-cmd", "score": 1.2569477427490174}, {"caption": "\\sidecaptionvpos{}{}", "snippet": "\\sidecaptionvpos{$1}{$2}", "meta": "sidecap-cmd", "score": 0.0006587927449241846}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "sidecap-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "sidecap-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "sidecap-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "sidecap-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "sidecap-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "sidecap-cmd", "score": 0.0018957469739775527}], "nomencl": [{"caption": "\\nomenclature[]{}{}", "snippet": "\\nomenclature[$1]{$2}{$3}", "meta": "nomencl-cmd", "score": 0.016053526743355948}, {"caption": "\\nomenclature{}{}", "snippet": "\\nomenclature{$1}{$2}", "meta": "nomencl-cmd", "score": 0.016053526743355948}, {"caption": "\\nomlabel", "snippet": "\\nomlabel", "meta": "nomencl-cmd", "score": 6.353668036093916e-05}, {"caption": "\\printnomenclature", "snippet": "\\printnomenclature", "meta": "nomencl-cmd", "score": 0.0014526113324237952}, {"caption": "\\printnomenclature[]", "snippet": "\\printnomenclature[$1]", "meta": "nomencl-cmd", "score": 0.0014526113324237952}, {"caption": "\\makenomenclature", "snippet": "\\makenomenclature", "meta": "nomencl-cmd", "score": 0.002310610204652063}, {"caption": "\\nomgroup", "snippet": "\\nomgroup", "meta": "nomencl-cmd", "score": 0.0005549290951493257}, {"caption": "\\nomgroup[]{}", "snippet": "\\nomgroup[$1]{$2}", "meta": "nomencl-cmd", "score": 0.0005549290951493257}, {"caption": "\\nomname", "snippet": "\\nomname", "meta": "nomencl-cmd", "score": 0.0015092617929470952}, {"caption": "\\nompreamble", "snippet": "\\nompreamble", "meta": "nomencl-cmd", "score": 2.4350510995473236e-05}, {"caption": "\\nomentryend", "snippet": "\\nomentryend", "meta": "nomencl-cmd", "score": 0.000137692304514793}], "afterpage": [{"caption": "\\afterpage{}", "snippet": "\\afterpage{$1}", "meta": "afterpage-cmd", "score": 0.0018578070791608345}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "afterpage-cmd", "score": 0.1789117552185788}], "titling": [{"caption": "\\thanks{}", "snippet": "\\thanks{$1}", "meta": "titling-cmd", "score": 0.08382259880654083}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "titling-cmd", "score": 0.7504160124360846}, {"caption": "\\posttitle{}", "snippet": "\\posttitle{$1}", "meta": "titling-cmd", "score": 0.002507149245154055}, {"caption": "\\postdate{}", "snippet": "\\postdate{$1}", "meta": "titling-cmd", "score": 0.002139478682489868}, {"caption": "\\predate{}", "snippet": "\\predate{$1}", "meta": "titling-cmd", "score": 0.002139478682489868}, {"caption": "\\preauthor{}", "snippet": "\\preauthor{$1}", "meta": "titling-cmd", "score": 0.0023736543205198435}, {"caption": "\\postauthor{}", "snippet": "\\postauthor{$1}", "meta": "titling-cmd", "score": 0.0023736543205198435}, {"caption": "\\pretitle{}", "snippet": "\\pretitle{$1}", "meta": "titling-cmd", "score": 0.002507149245154055}], "wasysym": [{"caption": "\\checked", "snippet": "\\checked", "meta": "wasysym-cmd", "score": 0.0027792832228568255}, {"caption": "\\int", "snippet": "\\int", "meta": "wasysym-cmd", "score": 0.11946660537765894}, {"caption": "\\diameter", "snippet": "\\diameter", "meta": "wasysym-cmd", "score": 0.0001645367385856751}, {"caption": "\\CIRCLE", "snippet": "\\CIRCLE", "meta": "wasysym-cmd", "score": 0.000250667024953401}], "eurosym": [{"caption": "\\EUR{}", "snippet": "\\EUR{$1}", "meta": "eurosym-cmd", "score": 3.661595357097087e-05}], "caption2": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "caption2-cmd", "score": 0.00037306820619479756}, {"caption": "\\DeclareCaptionJustification{}{}", "snippet": "\\DeclareCaptionJustification{$1}{$2}", "meta": "caption2-cmd", "score": 0.0001872850414971473}, {"caption": "\\DeclareCaptionLabelSeparator{}{}", "snippet": "\\DeclareCaptionLabelSeparator{$1}{$2}", "meta": "caption2-cmd", "score": 0.0003890810058478364}, {"caption": "\\DeclareCaptionFormat{}{}", "snippet": "\\DeclareCaptionFormat{$1}{$2}", "meta": "caption2-cmd", "score": 0.0004717618449370015}, {"caption": "\\DeclareCaptionFont{}{}", "snippet": "\\DeclareCaptionFont{$1}{$2}", "meta": "caption2-cmd", "score": 5.0133404990680195e-05}, {"caption": "\\DeclareCaptionSubType[]{}", "snippet": "\\DeclareCaptionSubType[$1]{$2}", "meta": "caption2-cmd", "score": 0.0001872850414971473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "caption2-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "caption2-cmd", "score": 0.021170869458413965}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "caption2-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "caption2-cmd", "score": 0.02900783226643065}, {"caption": "\\string", "snippet": "\\string", "meta": "caption2-cmd", "score": 0.001042697111754002}, {"caption": "\\DeclareCaptionType{}[][]", "snippet": "\\DeclareCaptionType{$1}[$2][$3]", "meta": "caption2-cmd", "score": 0.00015256647321237863}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "caption2-cmd", "score": 0.00530510025314411}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "caption2-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "caption2-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "caption2-cmd", "score": 0.021473212893597875}], "amsbsy": [{"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "amsbsy-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "amsbsy-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "amsbsy-cmd", "score": 0.18137737738638837}, {"caption": "\\do", "snippet": "\\do", "meta": "amsbsy-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "amsbsy-cmd", "score": 0.0063276692758974925}], "CJK": [{"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "CJK-cmd", "score": 0.04598628699063736}], "makecell": [{"caption": "\\diaghead{}{}{}", "snippet": "\\diaghead{$1}{$2}{$3}", "meta": "makecell-cmd", "score": 2.0417817976377812e-05}, {"caption": "\\makecell{}", "snippet": "\\makecell{$1}", "meta": "makecell-cmd", "score": 0.005023670619810683}, {"caption": "\\makecell[]{}", "snippet": "\\makecell[$1]{$2}", "meta": "makecell-cmd", "score": 0.005023670619810683}, {"caption": "\\height", "snippet": "\\height", "meta": "makecell-cmd", "score": 0.0045883162478394055}, {"caption": "\\height{}", "snippet": "\\height{$1}", "meta": "makecell-cmd", "score": 0.0045883162478394055}, {"caption": "\\setcellgapes{}", "snippet": "\\setcellgapes{$1}", "meta": "makecell-cmd", "score": 0.0004960838428758984}, {"caption": "\\thead{}", "snippet": "\\thead{$1}", "meta": "makecell-cmd", "score": 0.0023087638254186797}, {"caption": "\\Gape[]", "snippet": "\\Gape[$1]", "meta": "makecell-cmd", "score": 0.000469300371741866}, {"caption": "\\theadgape{}", "snippet": "\\theadgape{$1}", "meta": "makecell-cmd", "score": 0.000234650185870933}, {"caption": "\\theadalign", "snippet": "\\theadalign", "meta": "makecell-cmd", "score": 0.0006746935448099005}, {"caption": "\\theadalign{}", "snippet": "\\theadalign{$1}", "meta": "makecell-cmd", "score": 0.0006746935448099005}, {"caption": "\\theadset{}", "snippet": "\\theadset{$1}", "meta": "makecell-cmd", "score": 0.0004400433589389675}, {"caption": "\\Xhline{}", "snippet": "\\Xhline{$1}", "meta": "makecell-cmd", "score": 0.0024175651338281096}, {"caption": "\\theadfont{}", "snippet": "\\theadfont{$1}", "meta": "makecell-cmd", "score": 0.0007935193556772338}, {"caption": "\\theadfont", "snippet": "\\theadfont", "meta": "makecell-cmd", "score": 0.0007935193556772338}, {"caption": "\\cellgape{}", "snippet": "\\cellgape{$1}", "meta": "makecell-cmd", "score": 0.000234650185870933}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "makecell-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "makecell-cmd", "score": 0.022224283488673075}, {"caption": "\\makegapedcells", "snippet": "\\makegapedcells", "meta": "makecell-cmd", "score": 0.000431467454221244}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "makecell-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "makecell-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "makecell-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "makecell-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "makecell-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "makecell-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "makecell-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "makecell-cmd", "score": 0.018615449342361392}], "xeCJK": [{"caption": "\\setCJKmonofont{}", "snippet": "\\setCJKmonofont{$1}", "meta": "xeCJK-cmd", "score": 0.0057178353252375245}, {"caption": "\\setCJKmainfont{}", "snippet": "\\setCJKmainfont{$1}", "meta": "xeCJK-cmd", "score": 0.006622926778590894}, {"caption": "\\setCJKmainfont[]{}", "snippet": "\\setCJKmainfont[$1]{$2}", "meta": "xeCJK-cmd", "score": 0.006622926778590894}, {"caption": "\\setCJKsansfont{}", "snippet": "\\setCJKsansfont{$1}", "meta": "xeCJK-cmd", "score": 0.0057178353252375245}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xeCJK-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xeCJK-cmd", "score": 0.2864294797053033}], "threeparttable": [{"caption": "\\csname", "snippet": "\\csname", "meta": "threeparttable-cmd", "score": 0.008565354665444157}, {"caption": "\\item", "snippet": "\\item", "meta": "threeparttable-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "threeparttable-cmd", "score": 3.800886892251021}], "dirtytalk": [{"caption": "\\say{}", "snippet": "\\say{$1}", "meta": "dirtytalk-cmd", "score": 0.010246289746417045}, {"caption": "\\empty", "snippet": "\\empty", "meta": "dirtytalk-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "dirtytalk-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "dirtytalk-cmd", "score": 0.008565354665444157}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "dirtytalk-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "dirtytalk-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "dirtytalk-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "dirtytalk-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "dirtytalk-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "dirtytalk-cmd", "score": 0.0018957469739775527}, {"caption": "\\empty", "snippet": "\\empty", "meta": "dirtytalk-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "dirtytalk-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "dirtytalk-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "dirtytalk-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "dirtytalk-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "dirtytalk-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "dirtytalk-cmd", "score": 0.021170869458413965}], "balance": [{"caption": "\\balance", "snippet": "\\balance", "meta": "balance-cmd", "score": 0.003629066156300264}, {"caption": "\\balance{}", "snippet": "\\balance{$1}", "meta": "balance-cmd", "score": 0.003629066156300264}], "minted": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\usemintedstyle{}", "snippet": "\\usemintedstyle{$1}", "meta": "minted-cmd", "score": 0.00184279823796158}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\inputminted[]{}{}", "snippet": "\\inputminted[$1]{$2}{$3}", "meta": "minted-cmd", "score": 0.0016501519191680601}, {"caption": "\\inputminted{}{}", "snippet": "\\inputminted{$1}{$2}", "meta": "minted-cmd", "score": 0.0016501519191680601}, {"caption": "\\setminted[]{}", "snippet": "\\setminted[$1]{$2}", "meta": "minted-cmd", "score": 0.0004017914210172805}, {"caption": "\\setminted{}", "snippet": "\\setminted{$1}", "meta": "minted-cmd", "score": 0.0004017914210172805}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "minted-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "minted-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "minted-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "minted-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "minted-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "minted-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\listof{}{}", "snippet": "\\listof{$1}{$2}", "meta": "minted-cmd", "score": 0.0009837365348002915}, {"caption": "\\floatplacement{}{}", "snippet": "\\floatplacement{$1}{$2}", "meta": "minted-cmd", "score": 0.0005815474978918903}, {"caption": "\\restylefloat{}", "snippet": "\\restylefloat{$1}", "meta": "minted-cmd", "score": 0.0008866338267686714}, {"caption": "\\floatstyle{}", "snippet": "\\floatstyle{$1}", "meta": "minted-cmd", "score": 0.0015470917047414941}, {"caption": "\\floatname{}{}", "snippet": "\\floatname{$1}{$2}", "meta": "minted-cmd", "score": 0.0011934321931750752}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "minted-cmd", "score": 1.2569477427490174}, {"caption": "\\newfloat{}{}{}", "snippet": "\\newfloat{$1}{$2}{$3}", "meta": "minted-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat", "snippet": "\\newfloat", "meta": "minted-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat{}", "snippet": "\\newfloat{$1}", "meta": "minted-cmd", "score": 0.0012745874472536625}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "minted-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "minted-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "minted-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "minted-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "minted-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "minted-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "minted-cmd", "score": 0.028955796305270766}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "minted-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "minted-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "minted-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "minted-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "minted-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "minted-cmd", "score": 0.0018957469739775527}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\fbox{}", "snippet": "\\fbox{$1}", "meta": "minted-cmd", "score": 0.020865450075016792}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "minted-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\pagewiselinenumbers", "snippet": "\\pagewiselinenumbers", "meta": "minted-cmd", "score": 0.00016870831850106035}, {"caption": "\\linenomath", "snippet": "\\linenomath", "meta": "minted-cmd", "score": 1.4517338420208715e-05}, {"caption": "\\linenumberfont{}", "snippet": "\\linenumberfont{$1}", "meta": "minted-cmd", "score": 0.0001811784338695797}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\endlinenomath", "snippet": "\\endlinenomath", "meta": "minted-cmd", "score": 1.4517338420208715e-05}, {"caption": "\\nolinenumbers", "snippet": "\\nolinenumbers", "meta": "minted-cmd", "score": 0.0009805246614299932}, {"caption": "\\path", "snippet": "\\path", "meta": "minted-cmd", "score": 0.028200474217322108}, {"caption": "\\path[]", "snippet": "\\path[$1]", "meta": "minted-cmd", "score": 0.028200474217322108}, {"caption": "\\path{}", "snippet": "\\path{$1}", "meta": "minted-cmd", "score": 0.028200474217322108}, {"caption": "\\filedate{}", "snippet": "\\filedate{$1}", "meta": "minted-cmd", "score": 0.000578146635331119}, {"caption": "\\filedate", "snippet": "\\filedate", "meta": "minted-cmd", "score": 0.000578146635331119}, {"caption": "\\linenumbers", "snippet": "\\linenumbers", "meta": "minted-cmd", "score": 0.004687680659497865}, {"caption": "\\modulolinenumbers[]", "snippet": "\\modulolinenumbers[$1]", "meta": "minted-cmd", "score": 0.0027194991933605197}, {"caption": "\\fileversion{}", "snippet": "\\fileversion{$1}", "meta": "minted-cmd", "score": 0.000578146635331119}, {"caption": "\\fileversion", "snippet": "\\fileversion", "meta": "minted-cmd", "score": 0.000578146635331119}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "minted-cmd", "score": 0.021170869458413965}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "minted-cmd", "score": 0.002140559856649122}, {"caption": "\\VerbatimEnvironment", "snippet": "\\VerbatimEnvironment", "meta": "minted-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\fvset{}", "snippet": "\\fvset{$1}", "meta": "minted-cmd", "score": 0.00015476887282479622}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "minted-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "minted-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "minted-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "minted-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "minted-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "minted-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "minted-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "minted-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "minted-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "minted-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "minted-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "minted-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "minted-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "minted-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "minted-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "minted-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "minted-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "minted-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "minted-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "minted-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "minted-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "minted-cmd", "score": 0.008565354665444157}], "xifthen": [{"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "xifthen-cmd", "score": 0.01590723355124104}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "xifthen-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "xifthen-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "xifthen-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "xifthen-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "xifthen-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "xifthen-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "xifthen-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "xifthen-cmd", "score": 0.0018957469739775527}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "xifthen-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "xifthen-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "xifthen-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xifthen-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xifthen-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "xifthen-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "xifthen-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "xifthen-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "xifthen-cmd", "score": 0.028955796305270766}], "relsize": [{"caption": "\\mathlarger{}", "snippet": "\\mathlarger{$1}", "meta": "relsize-cmd", "score": 0.0031475241540308316}, {"caption": "\\smaller", "snippet": "\\smaller", "meta": "relsize-cmd", "score": 0.001271007880944704}], "epsf": [{"caption": "\\epsfbox{}", "snippet": "\\epsfbox{$1}", "meta": "epsf-cmd", "score": 0.00013712781345832882}], "datetime": [{"caption": "\\shortmonthname[]", "snippet": "\\shortmonthname[$1]", "meta": "datetime-cmd", "score": 0.00018524143860552933}, {"caption": "\\THEYEAR", "snippet": "\\THEYEAR", "meta": "datetime-cmd", "score": 8.638115929876123e-05}, {"caption": "\\currenttime", "snippet": "\\currenttime", "meta": "datetime-cmd", "score": 0.0002884868472087627}, {"caption": "\\monthname", "snippet": "\\monthname", "meta": "datetime-cmd", "score": 8.847106423071211e-05}, {"caption": "\\monthname[]", "snippet": "\\monthname[$1]", "meta": "datetime-cmd", "score": 8.847106423071211e-05}, {"caption": "\\today", "snippet": "\\today", "meta": "datetime-cmd", "score": 0.10733849317324783}, {"caption": "\\THEMONTH", "snippet": "\\THEMONTH", "meta": "datetime-cmd", "score": 8.638115929876123e-05}, {"caption": "\\yyyymmdddate", "snippet": "\\yyyymmdddate", "meta": "datetime-cmd", "score": 0.0002568405365040184}, {"caption": "\\pdfdate", "snippet": "\\pdfdate", "meta": "datetime-cmd", "score": 9.673490669434574e-05}, {"caption": "\\dateseparator", "snippet": "\\dateseparator", "meta": "datetime-cmd", "score": 0.00010966778823652713}, {"caption": "\\csname", "snippet": "\\csname", "meta": "datetime-cmd", "score": 0.008565354665444157}, {"caption": "\\THEDAY", "snippet": "\\THEDAY", "meta": "datetime-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\usdate", "snippet": "\\usdate", "meta": "datetime-cmd", "score": 0.00020980148911330757}, {"caption": "\\newdateformat{}{}", "snippet": "\\newdateformat{$1}{$2}", "meta": "datetime-cmd", "score": 8.638115929876123e-05}, {"caption": "\\settimeformat{}", "snippet": "\\settimeformat{$1}", "meta": "datetime-cmd", "score": 0.00010966778823652713}, {"caption": "\\csname", "snippet": "\\csname", "meta": "datetime-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "datetime-cmd", "score": 0.00037306820619479756}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "datetime-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "datetime-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "datetime-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "datetime-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "datetime-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "datetime-cmd", "score": 0.0018957469739775527}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "datetime-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "datetime-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "datetime-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "datetime-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "datetime-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "datetime-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "datetime-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "datetime-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "datetime-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "datetime-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "datetime-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "datetime-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "datetime-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "datetime-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "datetime-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "datetime-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "datetime-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "datetime-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "datetime-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "datetime-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "datetime-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "datetime-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "datetime-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "datetime-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "datetime-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "datetime-cmd", "score": 0.0063276692758974925}], "fontawesome": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "fontawesome-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "fontawesome-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "fontawesome-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "fontawesome-cmd", "score": 0.2864294797053033}], "forest": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "forest-cmd", "score": 0.00530510025314411}, {"caption": "\\bracketset{}", "snippet": "\\bracketset{$1}", "meta": "forest-cmd", "score": 0.00014301574866674164}, {"caption": "\\forestset{}", "snippet": "\\forestset{$1}", "meta": "forest-cmd", "score": 0.0020596473883671114}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "forest-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "forest-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "forest-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "forest-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "forest-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "forest-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "forest-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "forest-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "forest-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "forest-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "forest-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "forest-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "forest-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "forest-cmd", "score": 0.004649150613625593}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "forest-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "forest-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "forest-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "forest-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "forest-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "forest-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "forest-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "forest-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "forest-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "forest-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "forest-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "forest-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "forest-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "forest-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "forest-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "forest-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "forest-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "forest-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "forest-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "forest-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "forest-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "forest-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "forest-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "forest-cmd", "score": 0.2864294797053033}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "forest-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "forest-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "forest-cmd", "score": 0.004719094298848707}, {"caption": "\\reserveinserts{}", "snippet": "\\reserveinserts{$1}", "meta": "forest-cmd", "score": 0.0018653410309739879}, {"caption": "\\newtoks", "snippet": "\\newtoks", "meta": "forest-cmd", "score": 0.00031058155311734754}, {"caption": "\\csname", "snippet": "\\csname", "meta": "forest-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "forest-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "forest-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "forest-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "forest-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "forest-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "forest-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "forest-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "forest-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "forest-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "forest-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "forest-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "forest-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "forest-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "forest-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "forest-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "forest-cmd", "score": 0.2864294797053033}], "pgf": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgf-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgf-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgf-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgf-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgf-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgf-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgf-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgf-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgf-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgf-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgf-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgf-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgf-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgf-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgf-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgf-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgf-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgf-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgf-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgf-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgf-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgf-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgf-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgf-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgf-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgf-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgf-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgf-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgf-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgf-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgf-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgf-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgf-cmd", "score": 0.2864294797053033}], "pstricks": [{"caption": "\\green", "snippet": "\\green", "meta": "pstricks-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pstricks-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pstricks-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pstricks-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pstricks-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pstricks-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pstricks-cmd", "score": 0.006520475264573554}], "fancybox": [{"caption": "\\shadowbox{}", "snippet": "\\shadowbox{$1}", "meta": "fancybox-cmd", "score": 0.00107667147399019}, {"caption": "\\doublebox", "snippet": "\\doublebox", "meta": "fancybox-cmd", "score": 0.00015142240898356106}, {"caption": "\\VerbatimEnvironment", "snippet": "\\VerbatimEnvironment", "meta": "fancybox-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\thisfancypage{}{}", "snippet": "\\thisfancypage{$1}{$2}", "meta": "fancybox-cmd", "score": 0.00015142240898356106}, {"caption": "\\TheSbox", "snippet": "\\TheSbox", "meta": "fancybox-cmd", "score": 4.5350034239275855e-05}], "braket": [{"caption": "\\ket{}", "snippet": "\\ket{$1}", "meta": "braket-cmd", "score": 0.0326276280979336}, {"caption": "\\braket{}{}", "snippet": "\\braket{$1}{$2}", "meta": "braket-cmd", "score": 0.004421747491186916}, {"caption": "\\braket{}", "snippet": "\\braket{$1}", "meta": "braket-cmd", "score": 0.004421747491186916}, {"caption": "\\ketbra{}{}", "snippet": "\\ketbra{$1}{$2}", "meta": "braket-cmd", "score": 0.0006317858348936015}, {"caption": "\\ketbra", "snippet": "\\ketbra", "meta": "braket-cmd", "score": 0.0006317858348936015}, {"caption": "\\bra{}", "snippet": "\\bra{$1}", "meta": "braket-cmd", "score": 0.005609763332417241}, {"caption": "\\csname", "snippet": "\\csname", "meta": "braket-cmd", "score": 0.008565354665444157}], "import": [{"caption": "\\import{}{}", "snippet": "\\import{$1}{$2}", "meta": "import-cmd", "score": 0.1265354812350108}], "abntex2cite": [{"caption": "\\citeonline{}", "snippet": "\\citeonline{$1}", "meta": "abntex2cite-cmd", "score": 0.014277840409455324}, {"caption": "\\bibitem{}", "snippet": "\\bibitem{$1}", "meta": "abntex2cite-cmd", "score": 0.3689547570562042}, {"caption": "\\bibitem[]{}", "snippet": "\\bibitem[$1]{$2}", "meta": "abntex2cite-cmd", "score": 0.3689547570562042}, {"caption": "\\bibliographystyle{}", "snippet": "\\bibliographystyle{$1}", "meta": "abntex2cite-cmd", "score": 0.25122317941387773}, {"caption": "\\citeyear{}", "snippet": "\\citeyear{$1}", "meta": "abntex2cite-cmd", "score": 0.01091041305836494}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "abntex2cite-cmd", "score": 2.341195220791228}, {"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "abntex2cite-cmd", "score": 0.2659628337907604}, {"caption": "\\setstretch{}", "snippet": "\\setstretch{$1}", "meta": "abntex2cite-cmd", "score": 0.019634763572332112}, {"caption": "\\onehalfspacing", "snippet": "\\onehalfspacing", "meta": "abntex2cite-cmd", "score": 0.010655415521079565}, {"caption": "\\singlespacing", "snippet": "\\singlespacing", "meta": "abntex2cite-cmd", "score": 0.008351544612280968}, {"caption": "\\doublespacing", "snippet": "\\doublespacing", "meta": "abntex2cite-cmd", "score": 0.007835428951987135}, {"caption": "\\baselinestretch", "snippet": "\\baselinestretch", "meta": "abntex2cite-cmd", "score": 0.03225350148161425}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "abntex2cite-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "abntex2cite-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "abntex2cite-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "abntex2cite-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "abntex2cite-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "abntex2cite-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "abntex2cite-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "abntex2cite-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "abntex2cite-cmd", "score": 0.028955796305270766}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "abntex2cite-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "abntex2cite-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "abntex2cite-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "abntex2cite-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "abntex2cite-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "abntex2cite-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "abntex2cite-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "abntex2cite-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "abntex2cite-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "abntex2cite-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "abntex2cite-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "abntex2cite-cmd", "score": 0.00021116765384691477}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "abntex2cite-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "abntex2cite-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "abntex2cite-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "abntex2cite-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "abntex2cite-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "abntex2cite-cmd", "score": 0.0018957469739775527}], "isodate": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "isodate-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "isodate-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "isodate-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "isodate-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "isodate-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "isodate-cmd", "score": 0.0018957469739775527}], "tcolorbox": [{"caption": "\\tcbset{}", "snippet": "\\tcbset{$1}", "meta": "tcolorbox-cmd", "score": 0.00012246447222402193}, {"caption": "\\tcbuselibrary{}", "snippet": "\\tcbuselibrary{$1}", "meta": "tcolorbox-cmd", "score": 4.347671035621014e-05}, {"caption": "\\newtcolorbox[]{}[][]{}", "snippet": "\\newtcolorbox[$1]{$2}[$3][$4]{$5}", "meta": "tcolorbox-cmd", "score": 7.216282820556303e-05}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "tcolorbox-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "tcolorbox-cmd", "score": 0.022224283488673075}, {"caption": "\\newtcbox{}[][]{}", "snippet": "\\newtcbox{$1}[$2][$3]{$4}", "meta": "tcolorbox-cmd", "score": 3.558785984219631e-05}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tcolorbox-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tcolorbox-cmd", "score": 0.004719094298848707}, {"caption": "\\endverbatim", "snippet": "\\endverbatim", "meta": "tcolorbox-cmd", "score": 0.0022216421267780076}, {"caption": "\\verbatim", "snippet": "\\verbatim", "meta": "tcolorbox-cmd", "score": 0.0072203369120285256}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\par", "snippet": "\\par", "meta": "tcolorbox-cmd", "score": 0.413853376001159}, {"caption": "\\verbatiminput{}", "snippet": "\\verbatiminput{$1}", "meta": "tcolorbox-cmd", "score": 0.0024547099784948665}, {"caption": "\\verbatiminput", "snippet": "\\verbatiminput", "meta": "tcolorbox-cmd", "score": 0.0024547099784948665}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tcolorbox-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tcolorbox-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tcolorbox-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tcolorbox-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tcolorbox-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tcolorbox-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tcolorbox-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tcolorbox-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tcolorbox-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tcolorbox-cmd", "score": 0.004649150613625593}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "tcolorbox-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "tcolorbox-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "tcolorbox-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "tcolorbox-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "tcolorbox-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "tcolorbox-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "tcolorbox-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "tcolorbox-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "tcolorbox-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "tcolorbox-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "tcolorbox-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tcolorbox-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "tcolorbox-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "tcolorbox-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "tcolorbox-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tcolorbox-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tcolorbox-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tcolorbox-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tcolorbox-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tcolorbox-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tcolorbox-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tcolorbox-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tcolorbox-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tcolorbox-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tcolorbox-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tcolorbox-cmd", "score": 0.2864294797053033}], "vmargin": [{"caption": "\\setmargins{}", "snippet": "\\setmargins{$1}", "meta": "vmargin-cmd", "score": 3.138510306083217e-05}, {"caption": "\\setmarginsrb{}{}{}{}{}{}{}{}", "snippet": "\\setmarginsrb{$1}{$2}{$3}{$4}{$5}{$6}{$7}{$8}", "meta": "vmargin-cmd", "score": 0.0004759508676929243}, {"caption": "\\setpapersize{}", "snippet": "\\setpapersize{$1}", "meta": "vmargin-cmd", "score": 3.138510306083217e-05}], "mdframed": [{"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\newmdenv[]{}", "snippet": "\\newmdenv[$1]{$2}", "meta": "mdframed-cmd", "score": 0.0008776774843208122}, {"caption": "\\surroundwithmdframed[]{}", "snippet": "\\surroundwithmdframed[$1]{$2}", "meta": "mdframed-cmd", "score": 5.535446508489438e-05}, {"caption": "\\newmdtheoremenv{}{}", "snippet": "\\newmdtheoremenv{$1}{$2}", "meta": "mdframed-cmd", "score": 3.558785984219631e-05}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mdframed-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mdframed-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "mdframed-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "mdframed-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "mdframed-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "mdframed-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "mdframed-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "mdframed-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "mdframed-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "mdframed-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "mdframed-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "mdframed-cmd", "score": 0.2864294797053033}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mdframed-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mdframed-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "mdframed-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "mdframed-cmd", "score": 0.2864294797053033}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "mdframed-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "mdframed-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mdframed-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mdframed-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mdframed-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "mdframed-cmd", "score": 0.002958865219480927}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "mdframed-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "mdframed-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "mdframed-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "mdframed-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "mdframed-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "mdframed-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "mdframed-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "mdframed-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "mdframed-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "mdframed-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "mdframed-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "mdframed-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "mdframed-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "mdframed-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "mdframed-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "mdframed-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mdframed-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "mdframed-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "mdframed-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "mdframed-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "mdframed-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mdframed-cmd", "score": 0.008565354665444157}], "cancel": [{"caption": "\\cancel{}", "snippet": "\\cancel{$1}", "meta": "cancel-cmd", "score": 0.00017782514657538044}, {"caption": "\\cancelto{}{}", "snippet": "\\cancelto{$1}{$2}", "meta": "cancel-cmd", "score": 7.809089624140706e-05}], "textcase": [{"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "textcase-cmd", "score": 2.341195220791228}], "libertine": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "libertine-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "libertine-cmd", "score": 0.008565354665444157}], "flushend": [{"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "flushend-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "flushend-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "flushend-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "flushend-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "flushend-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "flushend-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "flushend-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "flushend-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "flushend-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "flushend-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "flushend-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "flushend-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "flushend-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "flushend-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "flushend-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "flushend-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "flushend-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "flushend-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "flushend-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "flushend-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "flushend-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "flushend-cmd", "score": 0.008565354665444157}], "psfrag": [{"caption": "\\csname", "snippet": "\\csname", "meta": "psfrag-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "psfrag-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "psfrag-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "psfrag-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "psfrag-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "psfrag-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "psfrag-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "psfrag-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "psfrag-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "psfrag-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "psfrag-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "psfrag-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "psfrag-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "psfrag-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "psfrag-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "psfrag-cmd", "score": 0.004649150613625593}], "tablefootnote": [{"caption": "\\tablefootnote{}", "snippet": "\\tablefootnote{$1}", "meta": "tablefootnote-cmd", "score": 0.00017554048326570823}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "tablefootnote-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "tablefootnote-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "tablefootnote-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tablefootnote-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tablefootnote-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "tablefootnote-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "tablefootnote-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "tablefootnote-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "tablefootnote-cmd", "score": 0.028955796305270766}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tablefootnote-cmd", "score": 0.008565354665444157}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "tablefootnote-cmd", "score": 0.01590723355124104}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "tablefootnote-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "tablefootnote-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "tablefootnote-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "tablefootnote-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "tablefootnote-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "tablefootnote-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "tablefootnote-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "tablefootnote-cmd", "score": 0.0018957469739775527}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tablefootnote-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tablefootnote-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tablefootnote-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tablefootnote-cmd", "score": 0.021170869458413965}], "amstext": [{"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "amstext-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "amstext-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "amstext-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "amstext-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "amstext-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "amstext-cmd", "score": 0.0063276692758974925}], "units": [{"caption": "\\unitfrac{}{}", "snippet": "\\unitfrac{$1}{$2}", "meta": "units-cmd", "score": 0.0009264866770139672}, {"caption": "\\unitfrac[]{}{}", "snippet": "\\unitfrac[$1]{$2}{$3}", "meta": "units-cmd", "score": 0.0009264866770139672}, {"caption": "\\unit[]{}", "snippet": "\\unit[$1]{$2}", "meta": "units-cmd", "score": 0.028299796173135428}, {"caption": "\\unit{}", "snippet": "\\unit{$1}", "meta": "units-cmd", "score": 0.028299796173135428}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "units-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "units-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "units-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "units-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "units-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "units-cmd", "score": 0.0018957469739775527}, {"caption": "\\nicefrac{}{}", "snippet": "\\nicefrac{$1}{$2}", "meta": "units-cmd", "score": 0.0018011350423659288}], "scrextend": [{"caption": "\\footref{}", "snippet": "\\footref{$1}", "meta": "scrextend-cmd", "score": 0.0003680857021151614}, {"caption": "\\footref", "snippet": "\\footref", "meta": "scrextend-cmd", "score": 0.0003680857021151614}, {"caption": "\\scriptsize", "snippet": "\\scriptsize", "meta": "scrextend-cmd", "score": 0.05550618634921613}, {"caption": "\\scriptsize{}", "snippet": "\\scriptsize{$1}", "meta": "scrextend-cmd", "score": 0.05550618634921613}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "scrextend-cmd", "score": 0.7504160124360846}, {"caption": "\\Large", "snippet": "\\Large", "meta": "scrextend-cmd", "score": 0.1987771081149759}, {"caption": "\\Large{}", "snippet": "\\Large{$1}", "meta": "scrextend-cmd", "score": 0.1987771081149759}, {"caption": "\\and", "snippet": "\\and", "meta": "scrextend-cmd", "score": 0.09847866956528724}, {"caption": "\\LARGE", "snippet": "\\LARGE", "meta": "scrextend-cmd", "score": 0.05947642043953873}, {"caption": "\\LARGE{}", "snippet": "\\LARGE{$1}", "meta": "scrextend-cmd", "score": 0.05947642043953873}, {"caption": "\\subtitle{}", "snippet": "\\subtitle{$1}", "meta": "scrextend-cmd", "score": 0.01803265454797817}, {"caption": "\\large", "snippet": "\\large", "meta": "scrextend-cmd", "score": 0.20377416734108866}, {"caption": "\\large{}", "snippet": "\\large{$1}", "meta": "scrextend-cmd", "score": 0.20377416734108866}, {"caption": "\\Huge", "snippet": "\\Huge", "meta": "scrextend-cmd", "score": 0.04725806985998919}, {"caption": "\\footnotesize", "snippet": "\\footnotesize", "meta": "scrextend-cmd", "score": 0.2038592081252624}, {"caption": "\\footnotesize{}", "snippet": "\\footnotesize{$1}", "meta": "scrextend-cmd", "score": 0.2038592081252624}, {"caption": "\\small", "snippet": "\\small", "meta": "scrextend-cmd", "score": 0.2447632045426295}, {"caption": "\\small{}", "snippet": "\\small{$1}", "meta": "scrextend-cmd", "score": 0.2447632045426295}, {"caption": "\\huge", "snippet": "\\huge", "meta": "scrextend-cmd", "score": 0.04229832859754922}, {"caption": "\\huge{}", "snippet": "\\huge{$1}", "meta": "scrextend-cmd", "score": 0.04229832859754922}, {"caption": "\\cleardoublepage", "snippet": "\\cleardoublepage", "meta": "scrextend-cmd", "score": 0.044016804142963585}, {"caption": "\\tiny{}", "snippet": "\\tiny{$1}", "meta": "scrextend-cmd", "score": 0.047727606910742924}, {"caption": "\\tiny", "snippet": "\\tiny", "meta": "scrextend-cmd", "score": 0.047727606910742924}, {"caption": "\\deffootnote[]{}{}{}", "snippet": "\\deffootnote[$1]{$2}{$3}{$4}", "meta": "scrextend-cmd", "score": 2.545393270896533e-05}, {"caption": "\\thefootnote", "snippet": "\\thefootnote", "meta": "scrextend-cmd", "score": 0.007676927812687567}, {"caption": "\\thefootnote{}", "snippet": "\\thefootnote{$1}", "meta": "scrextend-cmd", "score": 0.007676927812687567}, {"caption": "\\normalsize", "snippet": "\\normalsize", "meta": "scrextend-cmd", "score": 0.14261697855738878}, {"caption": "\\normalsize{}", "snippet": "\\normalsize{$1}", "meta": "scrextend-cmd", "score": 0.14261697855738878}, {"caption": "\\titlefont", "snippet": "\\titlefont", "meta": "scrextend-cmd", "score": 0.0005278519180709353}, {"caption": "\\thefootnotemark", "snippet": "\\thefootnotemark", "meta": "scrextend-cmd", "score": 2.545393270896533e-05}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "scrextend-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "scrextend-cmd", "score": 0.1789117552185788}, {"caption": "\\addtokomafont{}{}", "snippet": "\\addtokomafont{$1}{$2}", "meta": "scrextend-cmd", "score": 0.0008555564394100388}, {"caption": "\\setkomafont{}{}", "snippet": "\\setkomafont{$1}{$2}", "meta": "scrextend-cmd", "score": 0.012985816912639263}, {"caption": "\\KOMAoptions{}", "snippet": "\\KOMAoptions{$1}", "meta": "scrextend-cmd", "score": 0.000396664302361659}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "scrextend-cmd", "score": 0.00037306820619479756}], "mwe": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mwe-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mwe-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "mwe-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "mwe-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "mwe-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "mwe-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "mwe-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mwe-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "mwe-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mwe-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "mwe-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "mwe-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "mwe-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "mwe-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "mwe-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "mwe-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "mwe-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "mwe-cmd", "score": 0.004719094298848707}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "mwe-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mwe-cmd", "score": 0.008565354665444157}], "beamerposter": [{"caption": "\\scriptsize", "snippet": "\\scriptsize", "meta": "beamerposter-cmd", "score": 0.05550618634921613}, {"caption": "\\scriptsize{}", "snippet": "\\scriptsize{$1}", "meta": "beamerposter-cmd", "score": 0.05550618634921613}, {"caption": "\\Large", "snippet": "\\Large", "meta": "beamerposter-cmd", "score": 0.1987771081149759}, {"caption": "\\Large{}", "snippet": "\\Large{$1}", "meta": "beamerposter-cmd", "score": 0.1987771081149759}, {"caption": "\\footnotesize", "snippet": "\\footnotesize", "meta": "beamerposter-cmd", "score": 0.2038592081252624}, {"caption": "\\footnotesize{}", "snippet": "\\footnotesize{$1}", "meta": "beamerposter-cmd", "score": 0.2038592081252624}, {"caption": "\\LARGE", "snippet": "\\LARGE", "meta": "beamerposter-cmd", "score": 0.05947642043953873}, {"caption": "\\LARGE{}", "snippet": "\\LARGE{$1}", "meta": "beamerposter-cmd", "score": 0.05947642043953873}, {"caption": "\\large", "snippet": "\\large", "meta": "beamerposter-cmd", "score": 0.20377416734108866}, {"caption": "\\large{}", "snippet": "\\large{$1}", "meta": "beamerposter-cmd", "score": 0.20377416734108866}, {"caption": "\\VeryHuge", "snippet": "\\VeryHuge", "meta": "beamerposter-cmd", "score": 0.000892251826639951}, {"caption": "\\small", "snippet": "\\small", "meta": "beamerposter-cmd", "score": 0.2447632045426295}, {"caption": "\\small{}", "snippet": "\\small{$1}", "meta": "beamerposter-cmd", "score": 0.2447632045426295}, {"caption": "\\VERYHuge", "snippet": "\\VERYHuge", "meta": "beamerposter-cmd", "score": 0.0011668714784222325}, {"caption": "\\veryHuge", "snippet": "\\veryHuge", "meta": "beamerposter-cmd", "score": 0.000892251826639951}, {"caption": "\\normalsize", "snippet": "\\normalsize", "meta": "beamerposter-cmd", "score": 0.14261697855738878}, {"caption": "\\normalsize{}", "snippet": "\\normalsize{$1}", "meta": "beamerposter-cmd", "score": 0.14261697855738878}, {"caption": "\\tiny{}", "snippet": "\\tiny{$1}", "meta": "beamerposter-cmd", "score": 0.047727606910742924}, {"caption": "\\tiny", "snippet": "\\tiny", "meta": "beamerposter-cmd", "score": 0.047727606910742924}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "beamerposter-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "beamerposter-cmd", "score": 0.021170869458413965}], "footnote": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "footnote-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "footnote-cmd", "score": 0.021170869458413965}, {"caption": "\\makesavenoteenv{}", "snippet": "\\makesavenoteenv{$1}", "meta": "footnote-cmd", "score": 0.0018587414325895479}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "footnote-cmd", "score": 0.2253056071787701}, {"caption": "\\csname", "snippet": "\\csname", "meta": "footnote-cmd", "score": 0.008565354665444157}, {"caption": "\\parbox{}{}", "snippet": "\\parbox{$1}{$2}", "meta": "footnote-cmd", "score": 0.04800611019618169}], "invoice": [{"caption": "\\Fee{}{}{}", "snippet": "\\Fee{$1}{$2}{$3}", "meta": "invoice-cmd", "score": 0.003295435821387378}, {"caption": "\\ProjectTitle{}", "snippet": "\\ProjectTitle{$1}", "meta": "invoice-cmd", "score": 0.003295435821387378}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "invoice-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "invoice-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "invoice-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "invoice-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "invoice-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "invoice-cmd", "score": 0.0018957469739775527}, {"caption": "\\endhead", "snippet": "\\endhead", "meta": "invoice-cmd", "score": 0.0023853501147448834}, {"caption": "\\endfoot", "snippet": "\\endfoot", "meta": "invoice-cmd", "score": 0.00044045261916551967}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "invoice-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "invoice-cmd", "score": 0.021170869458413965}, {"caption": "\\nopagebreak", "snippet": "\\nopagebreak", "meta": "invoice-cmd", "score": 9.952664522415981e-05}, {"caption": "\\endfirsthead", "snippet": "\\endfirsthead", "meta": "invoice-cmd", "score": 0.0016148498709822416}, {"caption": "\\endlastfoot", "snippet": "\\endlastfoot", "meta": "invoice-cmd", "score": 0.00044045261916551967}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "invoice-cmd", "score": 0.3277033727934986}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "invoice-cmd", "score": 0.0029238994233674776}, {"caption": "\\pagebreak", "snippet": "\\pagebreak", "meta": "invoice-cmd", "score": 0.0313525090421608}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "invoice-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "invoice-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "invoice-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "invoice-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "invoice-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "invoice-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "invoice-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "invoice-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "invoice-cmd", "score": 0.028955796305270766}], "tikzpeople": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpeople-cmd", "score": 0.008565354665444157}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "tikzpeople-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikzpeople-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikzpeople-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "tikzpeople-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "tikzpeople-cmd", "score": 0.028955796305270766}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikzpeople-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikzpeople-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikzpeople-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikzpeople-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikzpeople-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzpeople-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikzpeople-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpeople-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikzpeople-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikzpeople-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikzpeople-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikzpeople-cmd", "score": 0.004719094298848707}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "tikzpeople-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "tikzpeople-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "tikzpeople-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "tikzpeople-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "tikzpeople-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "tikzpeople-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "tikzpeople-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "tikzpeople-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "tikzpeople-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "tikzpeople-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "tikzpeople-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzpeople-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "tikzpeople-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "tikzpeople-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "tikzpeople-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpeople-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikzpeople-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikzpeople-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikzpeople-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikzpeople-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzpeople-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikzpeople-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpeople-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikzpeople-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikzpeople-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikzpeople-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikzpeople-cmd", "score": 0.2864294797053033}], "titletoc": [{"caption": "\\thecontentspage", "snippet": "\\thecontentspage", "meta": "titletoc-cmd", "score": 0.0008054115902675176}, {"caption": "\\startcontents", "snippet": "\\startcontents", "meta": "titletoc-cmd", "score": 0.00026847053008917257}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "titletoc-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "titletoc-cmd", "score": 0.021170869458413965}, {"caption": "\\printcontents{}{}{}", "snippet": "\\printcontents{$1}{$2}{$3}", "meta": "titletoc-cmd", "score": 0.00013423526504458629}, {"caption": "\\titlecontents{}[]", "snippet": "\\titlecontents{$1}[$2]", "meta": "titletoc-cmd", "score": 0.0017036290423289926}, {"caption": "\\titlecontents{}[]{}{}{}{}[]", "snippet": "\\titlecontents{$1}[$2]{$3}{$4}{$5}{$6}[$7]", "meta": "titletoc-cmd", "score": 0.0017036290423289926}, {"caption": "\\titlecontents{}[]{}{}{}{}", "snippet": "\\titlecontents{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "titletoc-cmd", "score": 0.0017036290423289926}, {"caption": "\\numberline{}", "snippet": "\\numberline{$1}", "meta": "titletoc-cmd", "score": 0.007461440567272885}, {"caption": "\\dottedcontents{}[]{}{}{}", "snippet": "\\dottedcontents{$1}[$2]{$3}{$4}{$5}", "meta": "titletoc-cmd", "score": 4.743909531747666e-05}, {"caption": "\\filcenter", "snippet": "\\filcenter", "meta": "titletoc-cmd", "score": 0.0004835660211260246}, {"caption": "\\thecontentslabel", "snippet": "\\thecontentslabel", "meta": "titletoc-cmd", "score": 0.0010521864830662522}, {"caption": "\\contentsuse{}{}", "snippet": "\\contentsuse{$1}{$2}", "meta": "titletoc-cmd", "score": 6.110202388233705e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "titletoc-cmd", "score": 0.008565354665444157}, {"caption": "\\contentspage", "snippet": "\\contentspage", "meta": "titletoc-cmd", "score": 0.0004955116569277163}, {"caption": "\\contentslabel[]{}", "snippet": "\\contentslabel[$1]{$2}", "meta": "titletoc-cmd", "score": 0.0011055859582683105}, {"caption": "\\contentslabel{}", "snippet": "\\contentslabel{$1}", "meta": "titletoc-cmd", "score": 0.0011055859582683105}, {"caption": "\\contentsmargin{}", "snippet": "\\contentsmargin{$1}", "meta": "titletoc-cmd", "score": 0.00013423526504458629}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "titletoc-cmd", "score": 0.3277033727934986}, {"caption": "\\titlerule", "snippet": "\\titlerule", "meta": "titletoc-cmd", "score": 0.019273712561461216}, {"caption": "\\titlerule[]{}", "snippet": "\\titlerule[$1]{$2}", "meta": "titletoc-cmd", "score": 0.019273712561461216}], "dblfloatfix": [{"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "dblfloatfix-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "dblfloatfix-cmd", "score": 0.354445763583904}, {"caption": "\\textsubscript{}", "snippet": "\\textsubscript{$1}", "meta": "dblfloatfix-cmd", "score": 0.058405875394131175}, {"caption": "\\em", "snippet": "\\em", "meta": "dblfloatfix-cmd", "score": 0.10357353994640862}], "pgfplotstable": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfplotstable-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfplotstable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfplotstable-cmd", "score": 0.004719094298848707}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "pgfplotstable-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "pgfplotstable-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "pgfplotstable-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "pgfplotstable-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "pgfplotstable-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfplotstable-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "pgfplotstable-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.018615449342361392}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfplotstable-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfplotstable-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfplotstable-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfplotstable-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfplotstable-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfplotstable-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfplotstable-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfplotstable-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfplotstable-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfplotstable-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfplotstable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfplotstable-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfplotstable-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfplotstable-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfplotstable-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfplotstable-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfplotstable-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfplotstable-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfplotstable-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfplotstable-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfplotstable-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfplotstable-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfplotstable-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfplotstable-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfplotstable-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfplotstable-cmd", "score": 0.2864294797053033}], "acronym": [{"caption": "\\acp{}", "snippet": "\\acp{$1}", "meta": "acronym-cmd", "score": 0.0005185177930914685}, {"caption": "\\acsfont{}", "snippet": "\\acsfont{$1}", "meta": "acronym-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\aclabelfont", "snippet": "\\aclabelfont", "meta": "acronym-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\acro{}{}", "snippet": "\\acro{$1}{$2}", "meta": "acronym-cmd", "score": 0.023587207425038587}, {"caption": "\\acl{}", "snippet": "\\acl{$1}", "meta": "acronym-cmd", "score": 0.0008131607751426444}, {"caption": "\\acf{}", "snippet": "\\acf{$1}", "meta": "acronym-cmd", "score": 0.0006845634165950408}, {"caption": "\\acrodef{}[]{}", "snippet": "\\acrodef{$1}[$2]{$3}", "meta": "acronym-cmd", "score": 0.0002902047200830372}, {"caption": "\\acs{}", "snippet": "\\acs{$1}", "meta": "acronym-cmd", "score": 0.002351209826598939}, {"caption": "\\acfp{}", "snippet": "\\acfp{$1}", "meta": "acronym-cmd", "score": 2.2013599341265054e-05}, {"caption": "\\ac{}", "snippet": "\\ac{$1}", "meta": "acronym-cmd", "score": 0.04714113215364704}, {"caption": "\\let", "snippet": "\\let", "meta": "acronym-cmd", "score": 0.03789745970461662}], "nicefrac": [{"caption": "\\nicefrac{}{}", "snippet": "\\nicefrac{$1}{$2}", "meta": "nicefrac-cmd", "score": 0.0018011350423659288}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "nicefrac-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "nicefrac-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "nicefrac-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "nicefrac-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "nicefrac-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "nicefrac-cmd", "score": 0.0018957469739775527}], "smartdiagram": [{"caption": "\\usesmartdiagramlibrary{}", "snippet": "\\usesmartdiagramlibrary{$1}", "meta": "smartdiagram-cmd", "score": 7.216282820556303e-05}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "smartdiagram-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "smartdiagram-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "smartdiagram-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "smartdiagram-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "smartdiagram-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "smartdiagram-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "smartdiagram-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "smartdiagram-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "smartdiagram-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "smartdiagram-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "smartdiagram-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "smartdiagram-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "smartdiagram-cmd", "score": 0.004719094298848707}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "smartdiagram-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "smartdiagram-cmd", "score": 0.2864294797053033}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "smartdiagram-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "smartdiagram-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "smartdiagram-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "smartdiagram-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "smartdiagram-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "smartdiagram-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "smartdiagram-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "smartdiagram-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "smartdiagram-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "smartdiagram-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "smartdiagram-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "smartdiagram-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "smartdiagram-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "smartdiagram-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "smartdiagram-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "smartdiagram-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "smartdiagram-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "smartdiagram-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "smartdiagram-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "smartdiagram-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "smartdiagram-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "smartdiagram-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "smartdiagram-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "smartdiagram-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "smartdiagram-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "smartdiagram-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "smartdiagram-cmd", "score": 0.2864294797053033}], "qtree": [{"caption": "\\qroof{}", "snippet": "\\qroof{$1}", "meta": "qtree-cmd", "score": 0.00012663929287995903}, {"caption": "\\Tree[]", "snippet": "\\Tree[$1]", "meta": "qtree-cmd", "score": 0.0008894716589418522}, {"caption": "\\Tree", "snippet": "\\Tree", "meta": "qtree-cmd", "score": 0.0008894716589418522}], "backref": [{"caption": "\\backrefpagesname", "snippet": "\\backrefpagesname", "meta": "backref-cmd", "score": 0.0022756001200686213}, {"caption": "\\backref", "snippet": "\\backref", "meta": "backref-cmd", "score": 0.0025820187198826706}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "backref-cmd", "score": 0.1789117552185788}, {"caption": "\\global", "snippet": "\\global", "meta": "backref-cmd", "score": 0.006609629561859019}, {"caption": "\\csname", "snippet": "\\csname", "meta": "backref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "backref-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "backref-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "backref-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "backref-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "backref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "backref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "backref-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "backref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "backref-cmd", "score": 0.021170869458413965}, {"caption": "\\empty", "snippet": "\\empty", "meta": "backref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "backref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "backref-cmd", "score": 0.008565354665444157}, {"caption": "\\makeindex", "snippet": "\\makeindex", "meta": "backref-cmd", "score": 0.010304996748556729}, {"caption": "\\index{}", "snippet": "\\index{$1}", "meta": "backref-cmd", "score": 0.013774721817648336}, {"caption": "\\empty", "snippet": "\\empty", "meta": "backref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "backref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "backref-cmd", "score": 0.008565354665444157}], "epigraph": [{"caption": "\\epigraphflush{}", "snippet": "\\epigraphflush{$1}", "meta": "epigraph-cmd", "score": 1.8073688234300064e-05}, {"caption": "\\epigraphsize{}", "snippet": "\\epigraphsize{$1}", "meta": "epigraph-cmd", "score": 6.820709322498027e-05}, {"caption": "\\epigraphsize", "snippet": "\\epigraphsize", "meta": "epigraph-cmd", "score": 6.820709322498027e-05}, {"caption": "\\epigraph{}{}", "snippet": "\\epigraph{$1}{$2}", "meta": "epigraph-cmd", "score": 0.0031428856022970054}], "chngcntr": [{"caption": "\\counterwithin{}{}", "snippet": "\\counterwithin{$1}{$2}", "meta": "chngcntr-cmd", "score": 0.001287401394784382}, {"caption": "\\counterwithout{}{}", "snippet": "\\counterwithout{$1}{$2}", "meta": "chngcntr-cmd", "score": 0.0026127666246546326}], "empheq": [{"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "empheq-cmd", "score": 0.20852115286477566}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "empheq-cmd", "score": 1.897791904799601}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "empheq-cmd", "score": 0.051980653969641216}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "empheq-cmd", "score": 0.06345266254167037}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "empheq-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "empheq-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "empheq-cmd", "score": 0.18137737738638837}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "empheq-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "empheq-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "empheq-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "empheq-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "empheq-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "empheq-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "empheq-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "empheq-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "empheq-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "empheq-cmd", "score": 0.028955796305270766}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "empheq-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "empheq-cmd", "score": 0.021170869458413965}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "empheq-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "empheq-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "empheq-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "empheq-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "empheq-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "empheq-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "empheq-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "empheq-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "empheq-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "empheq-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "empheq-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "empheq-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "empheq-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "empheq-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "empheq-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "empheq-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "empheq-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "empheq-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "empheq-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "empheq-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "empheq-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "empheq-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "empheq-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "empheq-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "empheq-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "empheq-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "empheq-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "empheq-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "empheq-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "empheq-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "empheq-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "empheq-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "empheq-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "empheq-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "empheq-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "empheq-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "empheq-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "empheq-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "empheq-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "empheq-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "empheq-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "empheq-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "empheq-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "empheq-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "empheq-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "empheq-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "empheq-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "empheq-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "empheq-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "empheq-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "empheq-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "empheq-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "empheq-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "empheq-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "empheq-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "empheq-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "empheq-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "empheq-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "empheq-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "empheq-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "empheq-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "empheq-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "empheq-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "empheq-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "empheq-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "empheq-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "empheq-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "empheq-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "empheq-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "empheq-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "empheq-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "empheq-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "empheq-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "empheq-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "empheq-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "empheq-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "empheq-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "empheq-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "empheq-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "empheq-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "empheq-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "empheq-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "empheq-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "empheq-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "empheq-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "empheq-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "empheq-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "empheq-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "empheq-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "empheq-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "empheq-cmd", "score": 0.0058847868741168765}, {"caption": "\\xleftrightarrow[][]{}", "snippet": "\\xleftrightarrow[$1][$2]{$3}", "meta": "empheq-cmd", "score": 4.015559489911509e-05}, {"caption": "\\vcentcolon", "snippet": "\\vcentcolon", "meta": "empheq-cmd", "score": 0.00021361943526711615}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "empheq-cmd", "score": 0.0016148076375871775}, {"caption": "\\coloneqq", "snippet": "\\coloneqq", "meta": "empheq-cmd", "score": 0.0014407293323958122}, {"caption": "\\mathclap{}", "snippet": "\\mathclap{$1}", "meta": "empheq-cmd", "score": 7.84378567451772e-05}, {"caption": "\\adjustlimits", "snippet": "\\adjustlimits", "meta": "empheq-cmd", "score": 0.0005307066890271085}, {"caption": "\\MoveEqLeft", "snippet": "\\MoveEqLeft", "meta": "empheq-cmd", "score": 5.343949980628182e-05}, {"caption": "\\mathrlap{}", "snippet": "\\mathrlap{$1}", "meta": "empheq-cmd", "score": 0.0003112817211637952}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "empheq-cmd", "score": 0.051980653969641216}, {"caption": "\\xhookrightarrow{}", "snippet": "\\xhookrightarrow{$1}", "meta": "empheq-cmd", "score": 5.444260823474129e-05}, {"caption": "\\DeclarePairedDelimiter{}{}{}", "snippet": "\\DeclarePairedDelimiter{$1}{$2}{$3}", "meta": "empheq-cmd", "score": 0.0033916678416372487}, {"caption": "\\DeclarePairedDelimiter", "snippet": "\\DeclarePairedDelimiter", "meta": "empheq-cmd", "score": 0.0033916678416372487}, {"caption": "\\prescript{}{}{}", "snippet": "\\prescript{$1}{$2}{$3}", "meta": "empheq-cmd", "score": 8.833369785705982e-06}, {"caption": "\\underbrace{}", "snippet": "\\underbrace{$1}", "meta": "empheq-cmd", "score": 0.010373780436850907}, {"caption": "\\mathllap{}", "snippet": "\\mathllap{$1}", "meta": "empheq-cmd", "score": 3.140504277052775e-05}, {"caption": "\\overbrace{}", "snippet": "\\overbrace{$1}", "meta": "empheq-cmd", "score": 0.0006045704778718376}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "empheq-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "empheq-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "empheq-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "empheq-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "empheq-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "empheq-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "empheq-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "empheq-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "empheq-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "empheq-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "empheq-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "empheq-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "empheq-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "empheq-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "empheq-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "empheq-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "empheq-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "empheq-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "empheq-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "empheq-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "empheq-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "empheq-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "empheq-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "empheq-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "empheq-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "empheq-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "empheq-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "empheq-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "empheq-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "empheq-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "empheq-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "empheq-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "empheq-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "empheq-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "empheq-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "empheq-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "empheq-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "empheq-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "empheq-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "empheq-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "empheq-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "empheq-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "empheq-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "empheq-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "empheq-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "empheq-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "empheq-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "empheq-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "empheq-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "empheq-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "empheq-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "empheq-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "empheq-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "empheq-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "empheq-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "empheq-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "empheq-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "empheq-cmd", "score": 0.0063276692758974925}], "mathexam": [{"caption": "\\ExamInstrBox{}", "snippet": "\\ExamInstrBox{$1}", "meta": "mathexam-cmd", "score": 0.00035308240943436196}, {"caption": "\\ExamName{}", "snippet": "\\ExamName{$1}", "meta": "mathexam-cmd", "score": 0.00165391233892938}, {"caption": "\\ExamNameLine", "snippet": "\\ExamNameLine", "meta": "mathexam-cmd", "score": 0.00165391233892938}, {"caption": "\\ExamClass{}", "snippet": "\\ExamClass{$1}", "meta": "mathexam-cmd", "score": 0.00165391233892938}, {"caption": "\\ExamHead{}", "snippet": "\\ExamHead{$1}", "meta": "mathexam-cmd", "score": 0.00165391233892938}, {"caption": "\\answer{}", "snippet": "\\answer{$1}", "meta": "mathexam-cmd", "score": 0.0034436236729672894}, {"caption": "\\answer", "snippet": "\\answer", "meta": "mathexam-cmd", "score": 0.0034436236729672894}, {"caption": "\\lhead{}", "snippet": "\\lhead{$1}", "meta": "mathexam-cmd", "score": 0.05268978171228714}, {"caption": "\\chaptermark", "snippet": "\\chaptermark", "meta": "mathexam-cmd", "score": 0.005924520024686584}, {"caption": "\\chaptermark{}", "snippet": "\\chaptermark{$1}", "meta": "mathexam-cmd", "score": 0.005924520024686584}, {"caption": "\\fancypagestyle{}{}", "snippet": "\\fancypagestyle{$1}{$2}", "meta": "mathexam-cmd", "score": 0.009430919590937878}, {"caption": "\\footrule", "snippet": "\\footrule", "meta": "mathexam-cmd", "score": 0.0010032754348913366}, {"caption": "\\footrule{}", "snippet": "\\footrule{$1}", "meta": "mathexam-cmd", "score": 0.0010032754348913366}, {"caption": "\\fancyfoot[]{}", "snippet": "\\fancyfoot[$1]{$2}", "meta": "mathexam-cmd", "score": 0.024973618823189894}, {"caption": "\\fancyfoot{}", "snippet": "\\fancyfoot{$1}", "meta": "mathexam-cmd", "score": 0.024973618823189894}, {"caption": "\\fancyfootoffset[]{}", "snippet": "\\fancyfootoffset[$1]{$2}", "meta": "mathexam-cmd", "score": 0.0015373246231684555}, {"caption": "\\fancyfootoffset{}", "snippet": "\\fancyfootoffset{$1}", "meta": "mathexam-cmd", "score": 0.0015373246231684555}, {"caption": "\\footruleskip", "snippet": "\\footruleskip", "meta": "mathexam-cmd", "score": 0.000830117957327721}, {"caption": "\\fancyheadoffset[]{}", "snippet": "\\fancyheadoffset[$1]{$2}", "meta": "mathexam-cmd", "score": 0.0016786568695309166}, {"caption": "\\fancyheadoffset{}", "snippet": "\\fancyheadoffset{$1}", "meta": "mathexam-cmd", "score": 0.0016786568695309166}, {"caption": "\\iffloatpage{}{}", "snippet": "\\iffloatpage{$1}{$2}", "meta": "mathexam-cmd", "score": 6.606286310833368e-05}, {"caption": "\\cfoot{}", "snippet": "\\cfoot{$1}", "meta": "mathexam-cmd", "score": 0.013411641301057813}, {"caption": "\\subsectionmark", "snippet": "\\subsectionmark", "meta": "mathexam-cmd", "score": 3.1153423008593836e-05}, {"caption": "\\footrulewidth", "snippet": "\\footrulewidth", "meta": "mathexam-cmd", "score": 0.011424740897486949}, {"caption": "\\fancyhfoffset[]{}", "snippet": "\\fancyhfoffset[$1]{$2}", "meta": "mathexam-cmd", "score": 3.741978601121172e-05}, {"caption": "\\rhead{}", "snippet": "\\rhead{$1}", "meta": "mathexam-cmd", "score": 0.022782817416731292}, {"caption": "\\fancyplain{}{}", "snippet": "\\fancyplain{$1}{$2}", "meta": "mathexam-cmd", "score": 0.007402339896386138}, {"caption": "\\rfoot{}", "snippet": "\\rfoot{$1}", "meta": "mathexam-cmd", "score": 0.013393817825547868}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mathexam-cmd", "score": 0.00530510025314411}, {"caption": "\\plainheadrulewidth", "snippet": "\\plainheadrulewidth", "meta": "mathexam-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\baselinestretch", "snippet": "\\baselinestretch", "meta": "mathexam-cmd", "score": 0.03225350148161425}, {"caption": "\\lfoot{}", "snippet": "\\lfoot{$1}", "meta": "mathexam-cmd", "score": 0.00789399846642229}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "mathexam-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "mathexam-cmd", "score": 0.006776001543888959}, {"caption": "\\fancyhf{}", "snippet": "\\fancyhf{$1}", "meta": "mathexam-cmd", "score": 0.02314618933449356}, {"caption": "\\sectionmark", "snippet": "\\sectionmark", "meta": "mathexam-cmd", "score": 0.005008938879210868}, {"caption": "\\fancyhead[]{}", "snippet": "\\fancyhead[$1]{$2}", "meta": "mathexam-cmd", "score": 0.039101068064744296}, {"caption": "\\fancyhead{}", "snippet": "\\fancyhead{$1}", "meta": "mathexam-cmd", "score": 0.039101068064744296}, {"caption": "\\nouppercase{}", "snippet": "\\nouppercase{$1}", "meta": "mathexam-cmd", "score": 0.006416387071584083}, {"caption": "\\nouppercase", "snippet": "\\nouppercase", "meta": "mathexam-cmd", "score": 0.006416387071584083}, {"caption": "\\headrule", "snippet": "\\headrule", "meta": "mathexam-cmd", "score": 0.0008327432627715623}, {"caption": "\\headrule{}", "snippet": "\\headrule{$1}", "meta": "mathexam-cmd", "score": 0.0008327432627715623}, {"caption": "\\chead{}", "snippet": "\\chead{$1}", "meta": "mathexam-cmd", "score": 0.00755042164734884}, {"caption": "\\headrulewidth", "snippet": "\\headrulewidth", "meta": "mathexam-cmd", "score": 0.02268137935335823}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "mathexam-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "mathexam-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "mathexam-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "mathexam-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "mathexam-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "mathexam-cmd", "score": 0.0018957469739775527}, {"caption": "\\string", "snippet": "\\string", "meta": "mathexam-cmd", "score": 0.001042697111754002}], "floatrow": [{"caption": "\\floatfoot{}", "snippet": "\\floatfoot{$1}", "meta": "floatrow-cmd", "score": 0.0015365464531749851}, {"caption": "\\restylefloat{}", "snippet": "\\restylefloat{$1}", "meta": "floatrow-cmd", "score": 0.0008866338267686714}, {"caption": "\\floatsetup[]{}", "snippet": "\\floatsetup[$1]{$2}", "meta": "floatrow-cmd", "score": 0.0005456136119914056}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "floatrow-cmd", "score": 0.00037306820619479756}, {"caption": "\\DeclareCaptionJustification{}{}", "snippet": "\\DeclareCaptionJustification{$1}{$2}", "meta": "floatrow-cmd", "score": 0.0001872850414971473}, {"caption": "\\DeclareCaptionLabelSeparator{}{}", "snippet": "\\DeclareCaptionLabelSeparator{$1}{$2}", "meta": "floatrow-cmd", "score": 0.0003890810058478364}, {"caption": "\\DeclareCaptionFormat{}{}", "snippet": "\\DeclareCaptionFormat{$1}{$2}", "meta": "floatrow-cmd", "score": 0.0004717618449370015}, {"caption": "\\DeclareCaptionFont{}{}", "snippet": "\\DeclareCaptionFont{$1}{$2}", "meta": "floatrow-cmd", "score": 5.0133404990680195e-05}, {"caption": "\\DeclareCaptionSubType[]{}", "snippet": "\\DeclareCaptionSubType[$1]{$2}", "meta": "floatrow-cmd", "score": 0.0001872850414971473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "floatrow-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "floatrow-cmd", "score": 0.021170869458413965}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "floatrow-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "floatrow-cmd", "score": 0.02900783226643065}, {"caption": "\\string", "snippet": "\\string", "meta": "floatrow-cmd", "score": 0.001042697111754002}, {"caption": "\\DeclareCaptionType{}[][]", "snippet": "\\DeclareCaptionType{$1}[$2][$3]", "meta": "floatrow-cmd", "score": 0.00015256647321237863}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "floatrow-cmd", "score": 0.00530510025314411}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "floatrow-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "floatrow-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "floatrow-cmd", "score": 0.021473212893597875}], "scrpage2": [{"caption": "\\automark[]{}", "snippet": "\\automark[$1]{$2}", "meta": "scrpage2-cmd", "score": 0.0006703031783997437}, {"caption": "\\automark{}", "snippet": "\\automark{$1}", "meta": "scrpage2-cmd", "score": 0.0006703031783997437}, {"caption": "\\ofoot{}", "snippet": "\\ofoot{$1}", "meta": "scrpage2-cmd", "score": 0.000605620621498142}, {"caption": "\\ofoot[]{}", "snippet": "\\ofoot[$1]{$2}", "meta": "scrpage2-cmd", "score": 0.000605620621498142}, {"caption": "\\ohead{}", "snippet": "\\ohead{$1}", "meta": "scrpage2-cmd", "score": 0.004845161937670253}, {"caption": "\\ohead[]{}", "snippet": "\\ohead[$1]{$2}", "meta": "scrpage2-cmd", "score": 0.004845161937670253}, {"caption": "\\headfont", "snippet": "\\headfont", "meta": "scrpage2-cmd", "score": 0.0011116915941419892}, {"caption": "\\setheadsepline{}", "snippet": "\\setheadsepline{$1}", "meta": "scrpage2-cmd", "score": 0.00023538827295624133}, {"caption": "\\clearscrheadings", "snippet": "\\clearscrheadings", "meta": "scrpage2-cmd", "score": 0.0003679125016983611}, {"caption": "\\clearscrheadfoot", "snippet": "\\clearscrheadfoot", "meta": "scrpage2-cmd", "score": 0.000558377093879783}, {"caption": "\\pagemark", "snippet": "\\pagemark", "meta": "scrpage2-cmd", "score": 0.0017520841736604843}, {"caption": "\\chead{}", "snippet": "\\chead{$1}", "meta": "scrpage2-cmd", "score": 0.00755042164734884}, {"caption": "\\clearscrplain", "snippet": "\\clearscrplain", "meta": "scrpage2-cmd", "score": 0.00013252422874211978}, {"caption": "\\ifoot{}", "snippet": "\\ifoot{$1}", "meta": "scrpage2-cmd", "score": 0.0003620142864171218}, {"caption": "\\ifoot[]{}", "snippet": "\\ifoot[$1]{$2}", "meta": "scrpage2-cmd", "score": 0.0003620142864171218}, {"caption": "\\ihead{}", "snippet": "\\ihead{$1}", "meta": "scrpage2-cmd", "score": 0.0004507603139230655}, {"caption": "\\ihead[]{}", "snippet": "\\ihead[$1]{$2}", "meta": "scrpage2-cmd", "score": 0.0004507603139230655}, {"caption": "\\cfoot{}", "snippet": "\\cfoot{$1}", "meta": "scrpage2-cmd", "score": 0.013411641301057813}], "pbox": [{"caption": "\\pbox{}{}", "snippet": "\\pbox{$1}{$2}", "meta": "pbox-cmd", "score": 0.0010883030320478486}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "pbox-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "pbox-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "pbox-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "pbox-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "pbox-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "pbox-cmd", "score": 0.0018957469739775527}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "pbox-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "pbox-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "pbox-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pbox-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "pbox-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "pbox-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "pbox-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "pbox-cmd", "score": 0.028955796305270766}], "esint": [{"caption": "\\int", "snippet": "\\int", "meta": "esint-cmd", "score": 0.11946660537765894}, {"caption": "\\iint", "snippet": "\\iint", "meta": "esint-cmd", "score": 0.003916494384710151}, {"caption": "\\varoiint", "snippet": "\\varoiint", "meta": "esint-cmd", "score": 0.0001069175284516453}, {"caption": "\\iiint", "snippet": "\\iiint", "meta": "esint-cmd", "score": 0.0010383179918633135}, {"caption": "\\oint", "snippet": "\\oint", "meta": "esint-cmd", "score": 0.0028650540724050534}, {"caption": "\\oiint", "snippet": "\\oiint", "meta": "esint-cmd", "score": 7.127835230109687e-05}], "algorithmicx": [{"caption": "\\algrenewcommand", "snippet": "\\algrenewcommand", "meta": "algorithmicx-cmd", "score": 0.0019861803661869416}, {"caption": "\\Statex", "snippet": "\\Statex", "meta": "algorithmicx-cmd", "score": 0.008622777195102994}, {"caption": "\\BState{}", "snippet": "\\BState{$1}", "meta": "algorithmicx-cmd", "score": 0.0008685861525307122}, {"caption": "\\BState", "snippet": "\\BState", "meta": "algorithmicx-cmd", "score": 0.0008685861525307122}, {"caption": "\\algloopdefx{}[][]{}", "snippet": "\\algloopdefx{$1}[$2][$3]{$4}", "meta": "algorithmicx-cmd", "score": 0.00025315185701145097}, {"caption": "\\algnewcommand", "snippet": "\\algnewcommand", "meta": "algorithmicx-cmd", "score": 0.0030209395012065327}, {"caption": "\\algnewcommand{}[]{}", "snippet": "\\algnewcommand{$1}[$2]{$3}", "meta": "algorithmicx-cmd", "score": 0.0030209395012065327}, {"caption": "\\Comment{}", "snippet": "\\Comment{$1}", "meta": "algorithmicx-cmd", "score": 0.005178604573219454}, {"caption": "\\algblockdefx{}{}[]", "snippet": "\\algblockdefx{$1}{$2}[$3]", "meta": "algorithmicx-cmd", "score": 0.00025315185701145097}, {"caption": "\\algrenewtext{}{}", "snippet": "\\algrenewtext{$1}{$2}", "meta": "algorithmicx-cmd", "score": 0.0024415580558825975}, {"caption": "\\algrenewtext{}[]{}", "snippet": "\\algrenewtext{$1}[$2]{$3}", "meta": "algorithmicx-cmd", "score": 0.0024415580558825975}, {"caption": "\\algblock{}{}", "snippet": "\\algblock{$1}{$2}", "meta": "algorithmicx-cmd", "score": 0.0007916858220314837}, {"caption": "\\csname", "snippet": "\\csname", "meta": "algorithmicx-cmd", "score": 0.008565354665444157}, {"caption": "\\algdef{}[]{}{}{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "algorithmicx-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}{}[]{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}", "meta": "algorithmicx-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}[]{}", "snippet": "\\algdef{$1}[$2]{$3}[$4]{$5}", "meta": "algorithmicx-cmd", "score": 0.0003102486920966127}, {"caption": "\\algtext{}", "snippet": "\\algtext{$1}", "meta": "algorithmicx-cmd", "score": 0.0005463612015579842}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "algorithmicx-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "algorithmicx-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "algorithmicx-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "algorithmicx-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "algorithmicx-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "algorithmicx-cmd", "score": 0.0018957469739775527}], "bibentry": [{"caption": "\\bibentry{}", "snippet": "\\bibentry{$1}", "meta": "bibentry-cmd", "score": 0.002786693424998083}, {"caption": "\\url{}", "snippet": "\\url{$1}", "meta": "bibentry-cmd", "score": 0.13586474005868793}, {"caption": "\\item", "snippet": "\\item", "meta": "bibentry-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "bibentry-cmd", "score": 3.800886892251021}, {"caption": "\\nobibliography", "snippet": "\\nobibliography", "meta": "bibentry-cmd", "score": 0.0009870472135074372}, {"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "bibentry-cmd", "score": 0.2659628337907604}, {"caption": "\\doi{}", "snippet": "\\doi{$1}", "meta": "bibentry-cmd", "score": 0.004001210811454663}, {"caption": "\\doi", "snippet": "\\doi", "meta": "bibentry-cmd", "score": 0.004001210811454663}], "txfonts": [{"caption": "\\sqrt{}", "snippet": "\\sqrt{$1}", "meta": "txfonts-cmd", "score": 0.20240160977404634}], "ngerman": [{"caption": "\\figurename", "snippet": "\\figurename", "meta": "ngerman-cmd", "score": 0.008169568707145965}, {"caption": "\\figurename{}", "snippet": "\\figurename{$1}", "meta": "ngerman-cmd", "score": 0.008169568707145965}, {"caption": "\\indexname", "snippet": "\\indexname", "meta": "ngerman-cmd", "score": 0.0007544109314450072}, {"caption": "\\glqq", "snippet": "\\glqq", "meta": "ngerman-cmd", "score": 0.0039133256714254504}, {"caption": "\\glqq{}", "snippet": "\\glqq{$1}", "meta": "ngerman-cmd", "score": 0.0039133256714254504}, {"caption": "\\today", "snippet": "\\today", "meta": "ngerman-cmd", "score": 0.10733849317324783}, {"caption": "\\bibname", "snippet": "\\bibname", "meta": "ngerman-cmd", "score": 0.007599529252128519}, {"caption": "\\bibname{}", "snippet": "\\bibname{$1}", "meta": "ngerman-cmd", "score": 0.007599529252128519}, {"caption": "\\captionsngerman{}", "snippet": "\\captionsngerman{$1}", "meta": "ngerman-cmd", "score": 0.00010171098214158578}, {"caption": "\\grqq", "snippet": "\\grqq", "meta": "ngerman-cmd", "score": 0.006659522189248266}, {"caption": "\\grqq{}", "snippet": "\\grqq{$1}", "meta": "ngerman-cmd", "score": 0.006659522189248266}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "ngerman-cmd", "score": 0.0029238994233674776}], "eucal": [{"caption": "\\mathscr{}", "snippet": "\\mathscr{$1}", "meta": "eucal-cmd", "score": 0.025302230226027712}, {"caption": "\\mathcal{}", "snippet": "\\mathcal{$1}", "meta": "eucal-cmd", "score": 0.35084018920966636}], "ifluatex": [{"caption": "\\csname", "snippet": "\\csname", "meta": "ifluatex-cmd", "score": 0.008565354665444157}], "chemfig": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "chemfig-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemfig-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chemfig-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chemfig-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "chemfig-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "chemfig-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "chemfig-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "chemfig-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "chemfig-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chemfig-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "chemfig-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemfig-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "chemfig-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chemfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chemfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chemfig-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "chemfig-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chemfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chemfig-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chemfig-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "chemfig-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chemfig-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chemfig-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "chemfig-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "chemfig-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "chemfig-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "chemfig-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "chemfig-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chemfig-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "chemfig-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "chemfig-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemfig-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "chemfig-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "chemfig-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "chemfig-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "chemfig-cmd", "score": 0.2864294797053033}], "abstract": [{"caption": "\\abstractnamefont", "snippet": "\\abstractnamefont", "meta": "abstract-cmd", "score": 6.2350576842596716e-06}], "tikz-cd": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-cd-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-cd-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-cd-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikz-cd-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikz-cd-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikz-cd-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikz-cd-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-cd-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikz-cd-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-cd-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikz-cd-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-cd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-cd-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikz-cd-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-cd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-cd-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikz-cd-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-cd-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-cd-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikz-cd-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikz-cd-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikz-cd-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-cd-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikz-cd-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-cd-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikz-cd-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikz-cd-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikz-cd-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikz-cd-cmd", "score": 0.2864294797053033}], "flowfram": [{"caption": "\\framebreak", "snippet": "\\framebreak", "meta": "flowfram-cmd", "score": 0.004019097827091264}, {"caption": "\\newstaticframe{}{}{}{}", "snippet": "\\newstaticframe{$1}{$2}{$3}{$4}", "meta": "flowfram-cmd", "score": 0.0014762683341407986}, {"caption": "\\newflowframe{}{}{}{}[]", "snippet": "\\newflowframe{$1}{$2}{$3}{$4}[$5]", "meta": "flowfram-cmd", "score": 0.002952536668281597}, {"caption": "\\csname", "snippet": "\\csname", "meta": "flowfram-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "flowfram-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "flowfram-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "flowfram-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "flowfram-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "flowfram-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "flowfram-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "flowfram-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "flowfram-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "flowfram-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "flowfram-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "flowfram-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "flowfram-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "flowfram-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "flowfram-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "flowfram-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "flowfram-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "flowfram-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "flowfram-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "flowfram-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "flowfram-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "flowfram-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "flowfram-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "flowfram-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "flowfram-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "flowfram-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "flowfram-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "flowfram-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "flowfram-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "flowfram-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "flowfram-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "flowfram-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "flowfram-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "flowfram-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "flowfram-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "flowfram-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "flowfram-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "flowfram-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "flowfram-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "flowfram-cmd", "score": 0.004649150613625593}, {"caption": "\\afterpage{}", "snippet": "\\afterpage{$1}", "meta": "flowfram-cmd", "score": 0.0018578070791608345}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "flowfram-cmd", "score": 0.1789117552185788}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "flowfram-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "flowfram-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "flowfram-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "flowfram-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "flowfram-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "flowfram-cmd", "score": 0.0018957469739775527}], "marginnote": [{"caption": "\\marginnote{}", "snippet": "\\marginnote{$1}", "meta": "marginnote-cmd", "score": 0.010285502283803235}, {"caption": "\\marginnote", "snippet": "\\marginnote", "meta": "marginnote-cmd", "score": 0.010285502283803235}, {"caption": "\\raggedleftmarginnote", "snippet": "\\raggedleftmarginnote", "meta": "marginnote-cmd", "score": 0.0011268470793267921}], "xfrac": [{"caption": "\\sfrac{}{}", "snippet": "\\sfrac{$1}{$2}", "meta": "xfrac-cmd", "score": 0.0030164694688453453}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "xfrac-cmd", "score": 0.00037306820619479756}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "xfrac-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "xfrac-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "xfrac-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xfrac-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xfrac-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xfrac-cmd", "score": 0.2864294797053033}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xfrac-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xfrac-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xfrac-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xfrac-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xfrac-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xfrac-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "xfrac-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "xfrac-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "xfrac-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "xfrac-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "xfrac-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xfrac-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "xfrac-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xfrac-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "xfrac-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xfrac-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xfrac-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xfrac-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "xfrac-cmd", "score": 0.004649150613625593}, {"caption": "\\do", "snippet": "\\do", "meta": "xfrac-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "xfrac-cmd", "score": 0.0063276692758974925}], "shortvrb": [{"caption": "\\MakeShortVerb{}", "snippet": "\\MakeShortVerb{$1}", "meta": "shortvrb-cmd", "score": 0.0002890733176655595}, {"caption": "\\do", "snippet": "\\do", "meta": "shortvrb-cmd", "score": 0.009278344180101056}], "animate": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "animate-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "animate-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "animate-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "animate-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "animate-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "animate-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "animate-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "animate-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "animate-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "animate-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "animate-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "animate-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "animate-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "animate-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "animate-cmd", "score": 0.004649150613625593}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "animate-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "animate-cmd", "score": 0.2864294797053033}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "animate-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "animate-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "animate-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "animate-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "animate-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "animate-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "animate-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "animate-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "animate-cmd", "score": 0.028955796305270766}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "animate-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "animate-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "animate-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "animate-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "animate-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "animate-cmd", "score": 0.0018957469739775527}, {"caption": "\\csname", "snippet": "\\csname", "meta": "animate-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "animate-cmd", "score": 0.008565354665444157}], "euscript": [{"caption": "\\mathscr{}", "snippet": "\\mathscr{$1}", "meta": "euscript-cmd", "score": 0.025302230226027712}, {"caption": "\\mathcal{}", "snippet": "\\mathcal{$1}", "meta": "euscript-cmd", "score": 0.35084018920966636}], "hhline": [{"caption": "\\hhline{}", "snippet": "\\hhline{$1}", "meta": "hhline-cmd", "score": 0.0004816338278157677}], "subfiles": [{"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "subfiles-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "subfiles-cmd", "score": 1.4425339817971206}, {"caption": "\\subfile{}", "snippet": "\\subfile{$1}", "meta": "subfiles-cmd", "score": 0.03337062633525651}, {"caption": "\\endverbatim", "snippet": "\\endverbatim", "meta": "subfiles-cmd", "score": 0.0022216421267780076}, {"caption": "\\verbatim", "snippet": "\\verbatim", "meta": "subfiles-cmd", "score": 0.0072203369120285256}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "subfiles-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "subfiles-cmd", "score": 0.021170869458413965}, {"caption": "\\par", "snippet": "\\par", "meta": "subfiles-cmd", "score": 0.413853376001159}, {"caption": "\\verbatiminput{}", "snippet": "\\verbatiminput{$1}", "meta": "subfiles-cmd", "score": 0.0024547099784948665}, {"caption": "\\verbatiminput", "snippet": "\\verbatiminput", "meta": "subfiles-cmd", "score": 0.0024547099784948665}], "accents": [{"caption": "\\underaccent{}{}", "snippet": "\\underaccent{$1}{$2}", "meta": "accents-cmd", "score": 0.00109513727836357}], "theorem": [{"caption": "\\theorembodyfont{}", "snippet": "\\theorembodyfont{$1}", "meta": "theorem-cmd", "score": 0.00047103366488576113}], "metalogo": [{"caption": "\\XeTeX", "snippet": "\\XeTeX", "meta": "metalogo-cmd", "score": 0.0010635559050357936}, {"caption": "\\TeX", "snippet": "\\TeX", "meta": "metalogo-cmd", "score": 0.02873756018238537}, {"caption": "\\TeX{}", "snippet": "\\TeX{$1}", "meta": "metalogo-cmd", "score": 0.02873756018238537}, {"caption": "\\LaTeX", "snippet": "\\LaTeX", "meta": "metalogo-cmd", "score": 0.2334089308452787}, {"caption": "\\LaTeX{}", "snippet": "\\LaTeX{$1}", "meta": "metalogo-cmd", "score": 0.2334089308452787}, {"caption": "\\XeLaTeX", "snippet": "\\XeLaTeX", "meta": "metalogo-cmd", "score": 0.002009786035379175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "metalogo-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "metalogo-cmd", "score": 0.00037306820619479756}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "metalogo-cmd", "score": 0.00021116765384691477}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "metalogo-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "metalogo-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "metalogo-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "metalogo-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "metalogo-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "metalogo-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "metalogo-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "metalogo-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "metalogo-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "metalogo-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "metalogo-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "metalogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "metalogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "metalogo-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "metalogo-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "metalogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "metalogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "metalogo-cmd", "score": 0.004719094298848707}], "bookmark": [{"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\pdfbookmark[]{}{}", "snippet": "\\pdfbookmark[$1]{$2}{$3}", "meta": "bookmark-cmd", "score": 0.006492248863367502}, {"caption": "\\bookmarkget{}", "snippet": "\\bookmarkget{$1}", "meta": "bookmark-cmd", "score": 0.00026847053008917257}, {"caption": "\\bookmarksetup{}", "snippet": "\\bookmarksetup{$1}", "meta": "bookmark-cmd", "score": 0.001134118016265821}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "bookmark-cmd", "score": 0.00037306820619479756}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "bookmark-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "bookmark-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "bookmark-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "bookmark-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "bookmark-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "bookmark-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "bookmark-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "bookmark-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "bookmark-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "bookmark-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "bookmark-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "bookmark-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "bookmark-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\nameref{}", "snippet": "\\nameref{$1}", "meta": "bookmark-cmd", "score": 0.009472569279662113}, {"caption": "\\pdfbookmark[]{}{}", "snippet": "\\pdfbookmark[$1]{$2}{$3}", "meta": "bookmark-cmd", "score": 0.006492248863367502}, {"caption": "\\figureautorefname", "snippet": "\\figureautorefname", "meta": "bookmark-cmd", "score": 0.00014582556188448738}, {"caption": "\\figureautorefname{}", "snippet": "\\figureautorefname{$1}", "meta": "bookmark-cmd", "score": 0.00014582556188448738}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "bookmark-cmd", "score": 0.006963729684667191}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\footnoteautorefname", "snippet": "\\footnoteautorefname", "meta": "bookmark-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\roman{}", "snippet": "\\roman{$1}", "meta": "bookmark-cmd", "score": 0.005553384455935491}, {"caption": "\\roman", "snippet": "\\roman", "meta": "bookmark-cmd", "score": 0.005553384455935491}, {"caption": "\\string", "snippet": "\\string", "meta": "bookmark-cmd", "score": 0.001042697111754002}, {"caption": "\\MakeLowercase{}", "snippet": "\\MakeLowercase{$1}", "meta": "bookmark-cmd", "score": 0.017289599800633146}, {"caption": "\\textunderscore", "snippet": "\\textunderscore", "meta": "bookmark-cmd", "score": 0.001509072212764015}, {"caption": "\\do", "snippet": "\\do", "meta": "bookmark-cmd", "score": 0.009278344180101056}, {"caption": "\\begin{}", "snippet": "\\begin{$1}", "meta": "bookmark-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}[]", "snippet": "\\begin{$1}[$2]", "meta": "bookmark-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}{}", "snippet": "\\begin{$1}{$2}", "meta": "bookmark-cmd", "score": 7.849662248028187}, {"caption": "\\FancyVerbLineautorefname", "snippet": "\\FancyVerbLineautorefname", "meta": "bookmark-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\hyperlink{}{}", "snippet": "\\hyperlink{$1}{$2}", "meta": "bookmark-cmd", "score": 0.00978652043902115}, {"caption": "\\tableautorefname", "snippet": "\\tableautorefname", "meta": "bookmark-cmd", "score": 0.00012704528567339081}, {"caption": "\\tableautorefname{}", "snippet": "\\tableautorefname{$1}", "meta": "bookmark-cmd", "score": 0.00012704528567339081}, {"caption": "\\equationautorefname", "snippet": "\\equationautorefname", "meta": "bookmark-cmd", "score": 0.00018777198999871106}, {"caption": "\\equationautorefname{}", "snippet": "\\equationautorefname{$1}", "meta": "bookmark-cmd", "score": 0.00018777198999871106}, {"caption": "\\chapterautorefname", "snippet": "\\chapterautorefname", "meta": "bookmark-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\TeX", "snippet": "\\TeX", "meta": "bookmark-cmd", "score": 0.02873756018238537}, {"caption": "\\TeX{}", "snippet": "\\TeX{$1}", "meta": "bookmark-cmd", "score": 0.02873756018238537}, {"caption": "\\protect", "snippet": "\\protect", "meta": "bookmark-cmd", "score": 0.0200686676229443}, {"caption": "\\appendixautorefname", "snippet": "\\appendixautorefname", "meta": "bookmark-cmd", "score": 7.950698053641679e-05}, {"caption": "\\appendixautorefname{}", "snippet": "\\appendixautorefname{$1}", "meta": "bookmark-cmd", "score": 7.950698053641679e-05}, {"caption": "\\newlabel{}{}", "snippet": "\\newlabel{$1}{$2}", "meta": "bookmark-cmd", "score": 0.00029737672328168955}, {"caption": "\\texorpdfstring{}{}", "snippet": "\\texorpdfstring{$1}{$2}", "meta": "bookmark-cmd", "score": 0.0073781967296121}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "bookmark-cmd", "score": 0.002140559856649122}, {"caption": "\\alph", "snippet": "\\alph", "meta": "bookmark-cmd", "score": 0.01034327266194849}, {"caption": "\\alph{}", "snippet": "\\alph{$1}", "meta": "bookmark-cmd", "score": 0.01034327266194849}, {"caption": "\\pageref{}", "snippet": "\\pageref{$1}", "meta": "bookmark-cmd", "score": 0.019788865471151957}, {"caption": "\\item", "snippet": "\\item", "meta": "bookmark-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "bookmark-cmd", "score": 3.800886892251021}, {"caption": "\\LaTeX", "snippet": "\\LaTeX", "meta": "bookmark-cmd", "score": 0.2334089308452787}, {"caption": "\\LaTeX{}", "snippet": "\\LaTeX{$1}", "meta": "bookmark-cmd", "score": 0.2334089308452787}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\itemautorefname", "snippet": "\\itemautorefname", "meta": "bookmark-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "bookmark-cmd", "score": 1.2569477427490174}, {"caption": "\\sectionautorefname", "snippet": "\\sectionautorefname", "meta": "bookmark-cmd", "score": 0.0019832324299155183}, {"caption": "\\sectionautorefname{}", "snippet": "\\sectionautorefname{$1}", "meta": "bookmark-cmd", "score": 0.0019832324299155183}, {"caption": "\\LaTeXe", "snippet": "\\LaTeXe", "meta": "bookmark-cmd", "score": 0.007928096378157487}, {"caption": "\\LaTeXe{}", "snippet": "\\LaTeXe{$1}", "meta": "bookmark-cmd", "score": 0.007928096378157487}, {"caption": "\\footref{}", "snippet": "\\footref{$1}", "meta": "bookmark-cmd", "score": 0.0003680857021151614}, {"caption": "\\footref", "snippet": "\\footref", "meta": "bookmark-cmd", "score": 0.0003680857021151614}, {"caption": "\\hypertarget{}{}", "snippet": "\\hypertarget{$1}{$2}", "meta": "bookmark-cmd", "score": 0.009652820108904094}, {"caption": "\\theoremautorefname", "snippet": "\\theoremautorefname", "meta": "bookmark-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "bookmark-cmd", "score": 0.7504160124360846}, {"caption": "\\subparagraphautorefname", "snippet": "\\subparagraphautorefname", "meta": "bookmark-cmd", "score": 0.0005446476945175932}, {"caption": "\\url{}", "snippet": "\\url{$1}", "meta": "bookmark-cmd", "score": 0.13586474005868793}, {"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "bookmark-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "bookmark-cmd", "score": 0.8973590434087177}, {"caption": "\\href{}{}", "snippet": "\\href{$1}{$2}", "meta": "bookmark-cmd", "score": 0.27111130260612365}, {"caption": "\\Roman{}", "snippet": "\\Roman{$1}", "meta": "bookmark-cmd", "score": 0.0038703587462843594}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bookmark-cmd", "score": 0.00530510025314411}, {"caption": "\\autoref{}", "snippet": "\\autoref{$1}", "meta": "bookmark-cmd", "score": 0.03741172773691362}, {"caption": "\\nolinkurl{}", "snippet": "\\nolinkurl{$1}", "meta": "bookmark-cmd", "score": 0.0004995635515943437}, {"caption": "\\end{}", "snippet": "\\end{$1}", "meta": "bookmark-cmd", "score": 7.847906405228455}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "bookmark-cmd", "score": 0.0174633138331273}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "bookmark-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "bookmark-cmd", "score": 0.006776001543888959}, {"caption": "\\partautorefname", "snippet": "\\partautorefname", "meta": "bookmark-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\Itemautorefname{}", "snippet": "\\Itemautorefname{$1}", "meta": "bookmark-cmd", "score": 6.006262128895586e-05}, {"caption": "\\halign{}", "snippet": "\\halign{$1}", "meta": "bookmark-cmd", "score": 0.00017906650306643613}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "bookmark-cmd", "score": 0.20852115286477566}, {"caption": "\\ref{}", "snippet": "\\ref{$1}", "meta": "bookmark-cmd", "score": 1.4380093454211778}, {"caption": "\\Alph{}", "snippet": "\\Alph{$1}", "meta": "bookmark-cmd", "score": 0.002233258780143355}, {"caption": "\\Alph", "snippet": "\\Alph", "meta": "bookmark-cmd", "score": 0.002233258780143355}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "bookmark-cmd", "score": 0.047007158741781095}, {"caption": "\\MP", "snippet": "\\MP", "meta": "bookmark-cmd", "score": 0.00018344383742255004}, {"caption": "\\MP{}", "snippet": "\\MP{$1}", "meta": "bookmark-cmd", "score": 0.00018344383742255004}, {"caption": "\\paragraphautorefname", "snippet": "\\paragraphautorefname", "meta": "bookmark-cmd", "score": 0.0005446476945175932}, {"caption": "\\citeN{}", "snippet": "\\citeN{$1}", "meta": "bookmark-cmd", "score": 0.0018503938529945614}, {"caption": "\\citeN", "snippet": "\\citeN", "meta": "bookmark-cmd", "score": 0.0018503938529945614}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "bookmark-cmd", "score": 0.07503475348393239}, {"caption": "\\subsectionautorefname", "snippet": "\\subsectionautorefname", "meta": "bookmark-cmd", "score": 0.0012546605780895737}, {"caption": "\\subsectionautorefname{}", "snippet": "\\subsectionautorefname{$1}", "meta": "bookmark-cmd", "score": 0.0012546605780895737}, {"caption": "\\hyperref[]{}", "snippet": "\\hyperref[$1]{$2}", "meta": "bookmark-cmd", "score": 0.004515152477030062}, {"caption": "\\arabic{}", "snippet": "\\arabic{$1}", "meta": "bookmark-cmd", "score": 0.02445837629741638}, {"caption": "\\arabic", "snippet": "\\arabic", "meta": "bookmark-cmd", "score": 0.02445837629741638}, {"caption": "\\newline", "snippet": "\\newline", "meta": "bookmark-cmd", "score": 0.3311721696201715}, {"caption": "\\hypersetup{}", "snippet": "\\hypersetup{$1}", "meta": "bookmark-cmd", "score": 0.06967310843464661}, {"caption": "\\subsubsectionautorefname", "snippet": "\\subsubsectionautorefname", "meta": "bookmark-cmd", "score": 0.0012064581899162352}, {"caption": "\\subsubsectionautorefname{}", "snippet": "\\subsubsectionautorefname{$1}", "meta": "bookmark-cmd", "score": 0.0012064581899162352}, {"caption": "\\title{}", "snippet": "\\title{$1}", "meta": "bookmark-cmd", "score": 0.9202908262245683}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bookmark-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bookmark-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bookmark-cmd", "score": 0.00530510025314411}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bookmark-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bookmark-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "bookmark-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "bookmark-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "bookmark-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bookmark-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bookmark-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bookmark-cmd", "score": 0.00530510025314411}], "anysize": [{"caption": "\\marginsize{}{}{}{}", "snippet": "\\marginsize{$1}{$2}{$3}{$4}", "meta": "anysize-cmd", "score": 0.0012034744434699038}], "diagbox": [{"caption": "\\diagbox[]{}{}", "snippet": "\\diagbox[$1]{$2}{$3}", "meta": "diagbox-cmd", "score": 2.2176553306779127e-05}, {"caption": "\\backslashbox{}{}", "snippet": "\\backslashbox{$1}{$2}", "meta": "diagbox-cmd", "score": 0.0005060776550832729}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "diagbox-cmd", "score": 0.00037306820619479756}, {"caption": "\\Line", "snippet": "\\Line", "meta": "diagbox-cmd", "score": 0.0006078790177929149}, {"caption": "\\polygon", "snippet": "\\polygon", "meta": "diagbox-cmd", "score": 0.0008987552240147395}, {"caption": "\\line", "snippet": "\\line", "meta": "diagbox-cmd", "score": 0.014519741542622297}, {"caption": "\\polyline", "snippet": "\\polyline", "meta": "diagbox-cmd", "score": 0.00022468880600368487}, {"caption": "\\vector", "snippet": "\\vector", "meta": "diagbox-cmd", "score": 0.002970308722584179}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "diagbox-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "diagbox-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "diagbox-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "diagbox-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "diagbox-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "diagbox-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "diagbox-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "diagbox-cmd", "score": 0.018615449342361392}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "diagbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "diagbox-cmd", "score": 0.021170869458413965}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "diagbox-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "diagbox-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "diagbox-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "diagbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "diagbox-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "diagbox-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "diagbox-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "diagbox-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "diagbox-cmd", "score": 0.028955796305270766}], "commath": [{"caption": "\\dod{}{}", "snippet": "\\dod{$1}{$2}", "meta": "commath-cmd", "score": 7.950032807135384e-05}, {"caption": "\\dpd{}{}", "snippet": "\\dpd{$1}{$2}", "meta": "commath-cmd", "score": 0.00022966761442835552}, {"caption": "\\dpd[]{}{}", "snippet": "\\dpd[$1]{$2}{$3}", "meta": "commath-cmd", "score": 0.00022966761442835552}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "commath-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "commath-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "commath-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "commath-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "commath-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "commath-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "commath-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "commath-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "commath-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "commath-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "commath-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "commath-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "commath-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "commath-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "commath-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "commath-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "commath-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "commath-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "commath-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "commath-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "commath-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "commath-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "commath-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "commath-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "commath-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "commath-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "commath-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "commath-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "commath-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "commath-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "commath-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "commath-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "commath-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "commath-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "commath-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "commath-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "commath-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "commath-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "commath-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "commath-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "commath-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "commath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "commath-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "commath-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "commath-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "commath-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "commath-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "commath-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "commath-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "commath-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "commath-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "commath-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "commath-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "commath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "commath-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "commath-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "commath-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "commath-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "commath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "commath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "commath-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "commath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "commath-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "commath-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "commath-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "commath-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "commath-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "commath-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "commath-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "commath-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "commath-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "commath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "commath-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "commath-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "commath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "commath-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "commath-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "commath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "commath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "commath-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "commath-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "commath-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "commath-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "commath-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "commath-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "commath-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "commath-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "commath-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "commath-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "commath-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "commath-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "commath-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "commath-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "commath-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "commath-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "commath-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "commath-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "commath-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "commath-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "commath-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "commath-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "commath-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "commath-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "commath-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "commath-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "commath-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "commath-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "commath-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "commath-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "commath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "commath-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "commath-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "commath-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "commath-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "commath-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "commath-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "commath-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "commath-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "commath-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "commath-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "commath-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "commath-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "commath-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "commath-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "commath-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "commath-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "commath-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "commath-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "commath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "commath-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "commath-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "commath-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "commath-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "commath-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "commath-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "commath-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "commath-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "commath-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "commath-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "commath-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "commath-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "commath-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "commath-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "commath-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "commath-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "commath-cmd", "score": 0.0004286136584068833}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "commath-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "commath-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "commath-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "commath-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "commath-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "commath-cmd", "score": 0.0018957469739775527}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "commath-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "commath-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "commath-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "commath-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "commath-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "commath-cmd", "score": 0.0063276692758974925}], "breqn": [{"caption": "\\biggl", "snippet": "\\biggl", "meta": "breqn-cmd", "score": 0.0016066581118686831}, {"caption": "\\biggl[]", "snippet": "\\biggl[$1]", "meta": "breqn-cmd", "score": 0.0016066581118686831}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "breqn-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "breqn-cmd", "score": 0.015507614799858266}, {"caption": "\\end{}", "snippet": "\\end{$1}", "meta": "breqn-cmd", "score": 7.847906405228455}, {"caption": "\\Big", "snippet": "\\Big", "meta": "breqn-cmd", "score": 0.050370758781422345}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "breqn-cmd", "score": 0.04318078602869565}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "breqn-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "breqn-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "breqn-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "breqn-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "breqn-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "breqn-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "breqn-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "breqn-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "breqn-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "breqn-cmd", "score": 0.028955796305270766}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "breqn-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "breqn-cmd", "score": 0.2864294797053033}], "ClearSans": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "ClearSans-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ClearSans-cmd", "score": 0.008565354665444157}], "ccicons": [{"caption": "\\ccbynd", "snippet": "\\ccbynd", "meta": "ccicons-cmd", "score": 0.0002103469673225986}, {"caption": "\\ccbysa", "snippet": "\\ccbysa", "meta": "ccicons-cmd", "score": 0.00016986782584471025}], "varioref": [{"caption": "\\csname", "snippet": "\\csname", "meta": "varioref-cmd", "score": 0.008565354665444157}], "SIunits": [{"caption": "\\micro", "snippet": "\\micro", "meta": "SIunits-cmd", "score": 0.011051971930487929}, {"caption": "\\meter", "snippet": "\\meter", "meta": "SIunits-cmd", "score": 0.012499244923238213}, {"caption": "\\cdot", "snippet": "\\cdot", "meta": "SIunits-cmd", "score": 0.23029085545522762}, {"caption": "\\degreecelsius", "snippet": "\\degreecelsius", "meta": "SIunits-cmd", "score": 0.002130669712103909}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "SIunits-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "SIunits-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "SIunits-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "SIunits-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "SIunits-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "SIunits-cmd", "score": 0.0063276692758974925}], "alltt": [{"caption": "\\par", "snippet": "\\par", "meta": "alltt-cmd", "score": 0.413853376001159}], "fancyvrb": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "fancyvrb-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "fancyvrb-cmd", "score": 0.021170869458413965}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "fancyvrb-cmd", "score": 0.002140559856649122}, {"caption": "\\VerbatimEnvironment", "snippet": "\\VerbatimEnvironment", "meta": "fancyvrb-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "fancyvrb-cmd", "score": 0.008565354665444157}, {"caption": "\\fvset{}", "snippet": "\\fvset{$1}", "meta": "fancyvrb-cmd", "score": 0.00015476887282479622}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "fancyvrb-cmd", "score": 0.00037306820619479756}], "textgreek": [{"caption": "\\temp", "snippet": "\\temp", "meta": "textgreek-cmd", "score": 0.0003566413345844499}, {"caption": "\\temp{}", "snippet": "\\temp{$1}", "meta": "textgreek-cmd", "score": 0.0003566413345844499}], "endnotes": [{"caption": "\\endnote", "snippet": "\\endnote", "meta": "endnotes-cmd", "score": 4.002553629215439e-05}, {"caption": "\\theendnotes", "snippet": "\\theendnotes", "meta": "endnotes-cmd", "score": 0.0002788252334941383}], "leading": [{"caption": "\\leading{}", "snippet": "\\leading{$1}", "meta": "leading-cmd", "score": 0.00029077374894594517}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "leading-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "leading-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "leading-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "leading-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "leading-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "leading-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "leading-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "leading-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "leading-cmd", "score": 0.028955796305270766}], "esvect": [{"caption": "\\vv", "snippet": "\\vv", "meta": "esvect-cmd", "score": 0.003087420708479709}, {"caption": "\\vv{}", "snippet": "\\vv{$1}", "meta": "esvect-cmd", "score": 0.003087420708479709}], "lettrine": [{"caption": "\\LettrineFontHook", "snippet": "\\LettrineFontHook", "meta": "lettrine-cmd", "score": 9.103413871235853e-05}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "lettrine-cmd", "score": 0.20852115286477566}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "lettrine-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "lettrine-cmd", "score": 0.2864294797053033}, {"caption": "\\lettrine[]{}{}", "snippet": "\\lettrine[$1]{$2}{$3}", "meta": "lettrine-cmd", "score": 0.0028028146688245602}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "lettrine-cmd", "score": 0.00037306820619479756}], "pgfopts": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfopts-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfopts-cmd", "score": 0.021170869458413965}], "tabulary": [{"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "tabulary-cmd", "score": 0.014532521139459619}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "tabulary-cmd", "score": 0.5473606021405326}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tabulary-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tabulary-cmd", "score": 0.021170869458413965}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "tabulary-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "tabulary-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "tabulary-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "tabulary-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "tabulary-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tabulary-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "tabulary-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "tabulary-cmd", "score": 0.018615449342361392}], "grffile": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "grffile-cmd", "score": 0.00021116765384691477}, {"caption": "\\empty", "snippet": "\\empty", "meta": "grffile-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "grffile-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "grffile-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "grffile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "grffile-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "grffile-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "grffile-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "grffile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "grffile-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "grffile-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "grffile-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "grffile-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "grffile-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "grffile-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "grffile-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "grffile-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "grffile-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "grffile-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "grffile-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "grffile-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "grffile-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "grffile-cmd", "score": 0.004649150613625593}, {"caption": "\\csname", "snippet": "\\csname", "meta": "grffile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "grffile-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "grffile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "grffile-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "grffile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "grffile-cmd", "score": 0.021170869458413965}], "pgfgantt": [{"caption": "\\gantttitlecalendar{}", "snippet": "\\gantttitlecalendar{$1}", "meta": "pgfgantt-cmd", "score": 0.00027821409061195467}, {"caption": "\\ganttset{}", "snippet": "\\ganttset{$1}", "meta": "pgfgantt-cmd", "score": 0.0002492292297037303}, {"caption": "\\gantttitlelist[]{}{}", "snippet": "\\gantttitlelist[$1]{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.00046430963549633653}, {"caption": "\\gantttitlelist{}{}", "snippet": "\\gantttitlelist{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.00046430963549633653}, {"caption": "\\ganttlink[]{}{}", "snippet": "\\ganttlink[$1]{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.0011494045501518014}, {"caption": "\\newganttchartelement{}{}", "snippet": "\\newganttchartelement{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.00023651453263545777}, {"caption": "\\gantttitle{}{}", "snippet": "\\gantttitle{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.001804531670553746}, {"caption": "\\gantttitle[]{}{}", "snippet": "\\gantttitle[$1]{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.001804531670553746}, {"caption": "\\setganttlinklabel{}{}", "snippet": "\\setganttlinklabel{$1}{$2}", "meta": "pgfgantt-cmd", "score": 9.045112044064169e-05}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfgantt-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfgantt-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfgantt-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfgantt-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfgantt-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfgantt-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfgantt-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfgantt-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfgantt-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfgantt-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfgantt-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfgantt-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfgantt-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfgantt-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfgantt-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfgantt-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfgantt-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfgantt-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfgantt-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfgantt-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfgantt-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfgantt-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfgantt-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfgantt-cmd", "score": 0.2864294797053033}], "circuitikz": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "circuitikz-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "circuitikz-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "circuitikz-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "circuitikz-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "circuitikz-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "circuitikz-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "circuitikz-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "circuitikz-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "circuitikz-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "circuitikz-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "circuitikz-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "circuitikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "circuitikz-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "circuitikz-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "circuitikz-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "circuitikz-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "circuitikz-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "circuitikz-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "circuitikz-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "circuitikz-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "circuitikz-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "circuitikz-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "circuitikz-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "circuitikz-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "circuitikz-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "circuitikz-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "circuitikz-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "circuitikz-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "circuitikz-cmd", "score": 0.2864294797053033}], "hypcap": [{"caption": "\\csname", "snippet": "\\csname", "meta": "hypcap-cmd", "score": 0.008565354665444157}], "scrlayer-scrpage": [{"caption": "\\lofoot{}", "snippet": "\\lofoot{$1}", "meta": "scrlayer-scrpage-cmd", "score": 0.00011911213812243537}, {"caption": "\\rofoot{}", "snippet": "\\rofoot{$1}", "meta": "scrlayer-scrpage-cmd", "score": 0.00021082185485863327}, {"caption": "\\clearpairofpagestyles", "snippet": "\\clearpairofpagestyles", "meta": "scrlayer-scrpage-cmd", "score": 8.874602750594376e-05}, {"caption": "\\ihead{}", "snippet": "\\ihead{$1}", "meta": "scrlayer-scrpage-cmd", "score": 0.0004507603139230655}, {"caption": "\\ihead[]{}", "snippet": "\\ihead[$1]{$2}", "meta": "scrlayer-scrpage-cmd", "score": 0.0004507603139230655}, {"caption": "\\cofoot{}", "snippet": "\\cofoot{$1}", "meta": "scrlayer-scrpage-cmd", "score": 0.00021082185485863327}, {"caption": "\\cfoot{}", "snippet": "\\cfoot{$1}", "meta": "scrlayer-scrpage-cmd", "score": 0.013411641301057813}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "scrlayer-scrpage-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "scrlayer-scrpage-cmd", "score": 0.1789117552185788}, {"caption": "\\addtokomafont{}{}", "snippet": "\\addtokomafont{$1}{$2}", "meta": "scrlayer-scrpage-cmd", "score": 0.0008555564394100388}, {"caption": "\\setkomafont{}{}", "snippet": "\\setkomafont{$1}{$2}", "meta": "scrlayer-scrpage-cmd", "score": 0.012985816912639263}, {"caption": "\\KOMAoptions{}", "snippet": "\\KOMAoptions{$1}", "meta": "scrlayer-scrpage-cmd", "score": 0.000396664302361659}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "scrlayer-scrpage-cmd", "score": 0.00037306820619479756}, {"caption": "\\automark[]{}", "snippet": "\\automark[$1]{$2}", "meta": "scrlayer-scrpage-cmd", "score": 0.0006703031783997437}, {"caption": "\\automark{}", "snippet": "\\automark{$1}", "meta": "scrlayer-scrpage-cmd", "score": 0.0006703031783997437}, {"caption": "\\pagemark", "snippet": "\\pagemark", "meta": "scrlayer-scrpage-cmd", "score": 0.0017520841736604843}], "amsgen": [{"caption": "\\do", "snippet": "\\do", "meta": "amsgen-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "amsgen-cmd", "score": 0.0063276692758974925}], "tipa": [{"caption": "\\textipa{}", "snippet": "\\textipa{$1}", "meta": "tipa-cmd", "score": 0.0028202799587687334}], "appendixnumberbeamer": [{"caption": "\\appendix", "snippet": "\\appendix", "meta": "appendixnumberbeamer-cmd", "score": 0.047007158741781095}, {"caption": "\\inserttotalframenumber", "snippet": "\\inserttotalframenumber", "meta": "appendixnumberbeamer-cmd", "score": 0.0008756113669543194}], "totcount": [{"caption": "\\totvalue{}", "snippet": "\\totvalue{$1}", "meta": "totcount-cmd", "score": 0.000325977535138643}, {"caption": "\\newtotcounter{}", "snippet": "\\newtotcounter{$1}", "meta": "totcount-cmd", "score": 0.004398151085448998}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "totcount-cmd", "score": 0.00037306820619479756}], "atbegshi": [{"caption": "\\empty", "snippet": "\\empty", "meta": "atbegshi-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "atbegshi-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "atbegshi-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "atbegshi-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "atbegshi-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "atbegshi-cmd", "score": 0.008565354665444157}], "environ": [{"caption": "\\csname", "snippet": "\\csname", "meta": "environ-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "environ-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "environ-cmd", "score": 0.021170869458413965}], "arydshln": [{"caption": "\\hdashline", "snippet": "\\hdashline", "meta": "arydshln-cmd", "score": 3.1727559255976046e-05}, {"caption": "\\arrayrulecolor{}", "snippet": "\\arrayrulecolor{$1}", "meta": "arydshln-cmd", "score": 0.008538501902241319}, {"caption": "\\arrayrulecolor[]{}", "snippet": "\\arrayrulecolor[$1]{$2}", "meta": "arydshln-cmd", "score": 0.008538501902241319}, {"caption": "\\hline", "snippet": "\\hline", "meta": "arydshln-cmd", "score": 1.3209538327406387}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "arydshln-cmd", "score": 0.5473606021405326}, {"caption": "\\cline{}", "snippet": "\\cline{$1}", "meta": "arydshln-cmd", "score": 0.07276573550543858}], "fp": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "fp-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "fp-cmd", "score": 0.021170869458413965}], "here": [{"caption": "\\listof{}{}", "snippet": "\\listof{$1}{$2}", "meta": "here-cmd", "score": 0.0009837365348002915}, {"caption": "\\floatplacement{}{}", "snippet": "\\floatplacement{$1}{$2}", "meta": "here-cmd", "score": 0.0005815474978918903}, {"caption": "\\restylefloat{}", "snippet": "\\restylefloat{$1}", "meta": "here-cmd", "score": 0.0008866338267686714}, {"caption": "\\floatstyle{}", "snippet": "\\floatstyle{$1}", "meta": "here-cmd", "score": 0.0015470917047414941}, {"caption": "\\floatname{}{}", "snippet": "\\floatname{$1}{$2}", "meta": "here-cmd", "score": 0.0011934321931750752}, {"caption": "\\csname", "snippet": "\\csname", "meta": "here-cmd", "score": 0.008565354665444157}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "here-cmd", "score": 1.2569477427490174}, {"caption": "\\newfloat{}{}{}", "snippet": "\\newfloat{$1}{$2}{$3}", "meta": "here-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat", "snippet": "\\newfloat", "meta": "here-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat{}", "snippet": "\\newfloat{$1}", "meta": "here-cmd", "score": 0.0012745874472536625}], "layout": [{"caption": "\\layout", "snippet": "\\layout", "meta": "layout-cmd", "score": 0.0003951770756385293}, {"caption": "\\layout{}", "snippet": "\\layout{$1}", "meta": "layout-cmd", "score": 0.0003951770756385293}], "multibib": [{"caption": "\\newcites{}{}", "snippet": "\\newcites{$1}{$2}", "meta": "multibib-cmd", "score": 0.0024438508435048224}, {"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "multibib-cmd", "score": 0.2659628337907604}], "tgpagella": [{"caption": "\\empty", "snippet": "\\empty", "meta": "tgpagella-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgpagella-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgpagella-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgpagella-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgpagella-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgpagella-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgpagella-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgpagella-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tgpagella-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tgpagella-cmd", "score": 0.021170869458413965}], "minitoc": [{"caption": "\\addstarredchapter{}", "snippet": "\\addstarredchapter{$1}", "meta": "minitoc-cmd", "score": 0.0009796486230293261}, {"caption": "\\minitoc", "snippet": "\\minitoc", "meta": "minitoc-cmd", "score": 0.001626371504530358}, {"caption": "\\dominitoc", "snippet": "\\dominitoc", "meta": "minitoc-cmd", "score": 0.0006984399207241325}, {"caption": "\\mtcaddchapter", "snippet": "\\mtcaddchapter", "meta": "minitoc-cmd", "score": 9.045112044064169e-05}, {"caption": "\\listoffigures", "snippet": "\\listoffigures", "meta": "minitoc-cmd", "score": 0.03447318897846567}, {"caption": "\\listoftables", "snippet": "\\listoftables", "meta": "minitoc-cmd", "score": 0.02104656820469027}, {"caption": "\\tableofcontents", "snippet": "\\tableofcontents", "meta": "minitoc-cmd", "score": 0.13360595130994957}, {"caption": "\\adjustmtc", "snippet": "\\adjustmtc", "meta": "minitoc-cmd", "score": 0.00015075186740106945}, {"caption": "\\section{}", "snippet": "\\section{$1}", "meta": "minitoc-cmd", "score": 3.0952612541683835}], "nameref": [{"caption": "\\nameref{}", "snippet": "\\nameref{$1}", "meta": "nameref-cmd", "score": 0.009472569279662113}, {"caption": "\\protect", "snippet": "\\protect", "meta": "nameref-cmd", "score": 0.0200686676229443}, {"caption": "\\ref{}", "snippet": "\\ref{$1}", "meta": "nameref-cmd", "score": 1.4380093454211778}, {"caption": "\\pageref{}", "snippet": "\\pageref{$1}", "meta": "nameref-cmd", "score": 0.019788865471151957}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "nameref-cmd", "score": 1.897791904799601}, {"caption": "\\thepage", "snippet": "\\thepage", "meta": "nameref-cmd", "score": 0.0591555998103519}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nameref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "nameref-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "nameref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "nameref-cmd", "score": 0.021170869458413965}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "nameref-cmd", "score": 0.07503475348393239}, {"caption": "\\empty", "snippet": "\\empty", "meta": "nameref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "nameref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nameref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "nameref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "nameref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nameref-cmd", "score": 0.008565354665444157}], "ntheorem": [{"caption": "\\theoremclass{}", "snippet": "\\theoremclass{$1}", "meta": "ntheorem-cmd", "score": 0.0001448542182198375}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "ntheorem-cmd", "score": 0.06345266254167037}, {"caption": "\\theoremstyle{}", "snippet": "\\theoremstyle{$1}", "meta": "ntheorem-cmd", "score": 0.02533412165007986}, {"caption": "\\newshadedtheorem{}{}", "snippet": "\\newshadedtheorem{$1}{$2}", "meta": "ntheorem-cmd", "score": 0.0001632850673327423}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "ntheorem-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "ntheorem-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "ntheorem-cmd", "score": 0.215689795055434}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "ntheorem-cmd", "score": 1.897791904799601}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "ntheorem-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "ntheorem-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "ntheorem-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "ntheorem-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "ntheorem-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "ntheorem-cmd", "score": 0.0018957469739775527}], "tabto": [{"caption": "\\tab", "snippet": "\\tab", "meta": "tabto-cmd", "score": 0.016398493343291305}, {"caption": "\\tab{}", "snippet": "\\tab{$1}", "meta": "tabto-cmd", "score": 0.016398493343291305}, {"caption": "\\NumTabs{}", "snippet": "\\NumTabs{$1}", "meta": "tabto-cmd", "score": 0.00011350525217178113}, {"caption": "\\tabto{}{}", "snippet": "\\tabto{$1}{$2}", "meta": "tabto-cmd", "score": 0.002119919034744357}, {"caption": "\\tabto{}", "snippet": "\\tabto{$1}", "meta": "tabto-cmd", "score": 0.002119919034744357}], "emptypage": [{"caption": "\\cleardoublepage", "snippet": "\\cleardoublepage", "meta": "emptypage-cmd", "score": 0.044016804142963585}], "abntex2abrev": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "abntex2abrev-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "abntex2abrev-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "abntex2abrev-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "abntex2abrev-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "abntex2abrev-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "abntex2abrev-cmd", "score": 0.0018957469739775527}], "scrhack": [{"caption": "\\newpage", "snippet": "\\newpage", "meta": "scrhack-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "scrhack-cmd", "score": 0.1789117552185788}, {"caption": "\\addtokomafont{}{}", "snippet": "\\addtokomafont{$1}{$2}", "meta": "scrhack-cmd", "score": 0.0008555564394100388}, {"caption": "\\setkomafont{}{}", "snippet": "\\setkomafont{$1}{$2}", "meta": "scrhack-cmd", "score": 0.012985816912639263}, {"caption": "\\KOMAoptions{}", "snippet": "\\KOMAoptions{$1}", "meta": "scrhack-cmd", "score": 0.000396664302361659}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "scrhack-cmd", "score": 0.00037306820619479756}, {"caption": "\\xpatchcmd{}{}{}{}{}", "snippet": "\\xpatchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "scrhack-cmd", "score": 0.0019344877752147675}, {"caption": "\\xpatchcmd", "snippet": "\\xpatchcmd", "meta": "scrhack-cmd", "score": 0.0019344877752147675}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "scrhack-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "scrhack-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "scrhack-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "scrhack-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "scrhack-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "scrhack-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "scrhack-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "scrhack-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "scrhack-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "scrhack-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "scrhack-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "scrhack-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "scrhack-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "scrhack-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "scrhack-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "scrhack-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "scrhack-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "scrhack-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "scrhack-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "scrhack-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "scrhack-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "scrhack-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "scrhack-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "scrhack-cmd", "score": 0.2864294797053033}], "nth": [{"caption": "\\nth{}", "snippet": "\\nth{$1}", "meta": "nth-cmd", "score": 0.0006155314043974968}, {"caption": "\\thesection", "snippet": "\\thesection", "meta": "nth-cmd", "score": 0.011068945893347528}, {"caption": "\\thesection{}", "snippet": "\\thesection{$1}", "meta": "nth-cmd", "score": 0.011068945893347528}], "showkeys": [{"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "showkeys-cmd", "score": 1.897791904799601}], "fncychap": [{"caption": "\\appendix", "snippet": "\\appendix", "meta": "fncychap-cmd", "score": 0.047007158741781095}, {"caption": "\\ChTitleVar{}", "snippet": "\\ChTitleVar{$1}", "meta": "fncychap-cmd", "score": 0.00047530324346933345}, {"caption": "\\thechapter", "snippet": "\\thechapter", "meta": "fncychap-cmd", "score": 0.011821300392639589}], "ae": [{"caption": "\\sfdefault", "snippet": "\\sfdefault", "meta": "ae-cmd", "score": 0.008427383388519996}, {"caption": "\\sfdefault{}", "snippet": "\\sfdefault{$1}", "meta": "ae-cmd", "score": 0.008427383388519996}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "ae-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "ae-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "ae-cmd", "score": 0.021170869458413965}], "asymptote": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "asymptote-cmd", "score": 0.00037306820619479756}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "asymptote-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "asymptote-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "asymptote-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "asymptote-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "asymptote-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "asymptote-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "asymptote-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "asymptote-cmd", "score": 0.2864294797053033}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "asymptote-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "asymptote-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "asymptote-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "asymptote-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "asymptote-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "asymptote-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "asymptote-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "asymptote-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "asymptote-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "asymptote-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "asymptote-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "asymptote-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "asymptote-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "asymptote-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "asymptote-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "asymptote-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "asymptote-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "asymptote-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "asymptote-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "asymptote-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "asymptote-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "asymptote-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "asymptote-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "asymptote-cmd", "score": 0.004649150613625593}, {"caption": "\\csname", "snippet": "\\csname", "meta": "asymptote-cmd", "score": 0.008565354665444157}], "truncate": [{"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "truncate-cmd", "score": 0.04598628699063736}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "truncate-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "truncate-cmd", "score": 0.021170869458413965}], "xpatch": [{"caption": "\\xpatchcmd{}{}{}{}{}", "snippet": "\\xpatchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "xpatch-cmd", "score": 0.0019344877752147675}, {"caption": "\\xpatchcmd", "snippet": "\\xpatchcmd", "meta": "xpatch-cmd", "score": 0.0019344877752147675}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "xpatch-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "xpatch-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "xpatch-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "xpatch-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "xpatch-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "xpatch-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "xpatch-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "xpatch-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "xpatch-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "xpatch-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "xpatch-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "xpatch-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "xpatch-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "xpatch-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "xpatch-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "xpatch-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xpatch-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "xpatch-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "xpatch-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "xpatch-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "xpatch-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xpatch-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xpatch-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xpatch-cmd", "score": 0.2864294797053033}], "totpages": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "totpages-cmd", "score": 0.00037306820619479756}], "fourier": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "fourier-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "fourier-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "fourier-cmd", "score": 0.021170869458413965}], "scrbase": [{"caption": "\\newpage", "snippet": "\\newpage", "meta": "scrbase-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "scrbase-cmd", "score": 0.1789117552185788}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "scrbase-cmd", "score": 0.00037306820619479756}], "svg": [{"caption": "\\newpage", "snippet": "\\newpage", "meta": "svg-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "svg-cmd", "score": 0.1789117552185788}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "svg-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "svg-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "svg-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "svg-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "svg-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "svg-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "svg-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "svg-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "svg-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "svg-cmd", "score": 0.004719094298848707}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "svg-cmd", "score": 0.00021116765384691477}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "svg-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "svg-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "svg-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "svg-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "svg-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "svg-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "svg-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "svg-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "svg-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "svg-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "svg-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "svg-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "svg-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "svg-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "svg-cmd", "score": 0.004649150613625593}, {"caption": "\\csname", "snippet": "\\csname", "meta": "svg-cmd", "score": 0.008565354665444157}], "etex": [{"caption": "\\reserveinserts{}", "snippet": "\\reserveinserts{$1}", "meta": "etex-cmd", "score": 0.0018653410309739879}, {"caption": "\\newtoks", "snippet": "\\newtoks", "meta": "etex-cmd", "score": 0.00031058155311734754}], "linguex": [{"caption": "\\Last[]", "snippet": "\\Last[$1]", "meta": "linguex-cmd", "score": 0.0008163755131430334}, {"caption": "\\Last", "snippet": "\\Last", "meta": "linguex-cmd", "score": 0.0008163755131430334}, {"caption": "\\Next", "snippet": "\\Next", "meta": "linguex-cmd", "score": 0.0018776636802289772}, {"caption": "\\Next[]", "snippet": "\\Next[$1]", "meta": "linguex-cmd", "score": 0.0018776636802289772}, {"caption": "\\LLast[]", "snippet": "\\LLast[$1]", "meta": "linguex-cmd", "score": 0.00016327510262860667}, {"caption": "\\LLast", "snippet": "\\LLast", "meta": "linguex-cmd", "score": 0.00016327510262860667}, {"caption": "\\NNext[]", "snippet": "\\NNext[$1]", "meta": "linguex-cmd", "score": 0.0004490065322286684}, {"caption": "\\NNext", "snippet": "\\NNext", "meta": "linguex-cmd", "score": 0.0004490065322286684}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "linguex-cmd", "score": 1.897791904799601}, {"caption": "\\xspace", "snippet": "\\xspace", "meta": "linguex-cmd", "score": 0.07560370351316588}], "adforn": [{"caption": "\\adforn{}", "snippet": "\\adforn{$1}", "meta": "adforn-cmd", "score": 0.0003148505561835075}, {"caption": "\\ding{}", "snippet": "\\ding{$1}", "meta": "adforn-cmd", "score": 0.009992300665793867}], "bigstrut": [{"caption": "\\bigstrut", "snippet": "\\bigstrut", "meta": "bigstrut-cmd", "score": 0.005498219710082848}], "standalone": [{"caption": "\\renewcommand{}{}", "snippet": "\\renewcommand{$1}{$2}", "meta": "standalone-cmd", "score": 0.3267437011085663}, {"caption": "\\renewcommand", "snippet": "\\renewcommand", "meta": "standalone-cmd", "score": 0.3267437011085663}, {"caption": "\\currfiledir", "snippet": "\\currfiledir", "meta": "standalone-cmd", "score": 0.0002459788020229296}, {"caption": "\\empty", "snippet": "\\empty", "meta": "standalone-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "standalone-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "standalone-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "standalone-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "standalone-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "standalone-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "standalone-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "standalone-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "standalone-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "standalone-cmd", "score": 0.021170869458413965}], "ifsym": [{"caption": "\\Letter", "snippet": "\\Letter", "meta": "ifsym-cmd", "score": 0.0012281130571092198}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "ifsym-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "ifsym-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "ifsym-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "ifsym-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "ifsym-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "ifsym-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "ifsym-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "ifsym-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "ifsym-cmd", "score": 0.028955796305270766}], "newtxtext": [{"caption": "\\textsc{}", "snippet": "\\textsc{$1}", "meta": "newtxtext-cmd", "score": 0.6926466355384758}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "newtxtext-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "newtxtext-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "newtxtext-cmd", "score": 0.021170869458413965}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "newtxtext-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "newtxtext-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "newtxtext-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "newtxtext-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "newtxtext-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "newtxtext-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "newtxtext-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "newtxtext-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "newtxtext-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "newtxtext-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "newtxtext-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "newtxtext-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "newtxtext-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "newtxtext-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "newtxtext-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "newtxtext-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "newtxtext-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "newtxtext-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "newtxtext-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "newtxtext-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "newtxtext-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "newtxtext-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "newtxtext-cmd", "score": 0.008565354665444157}], "silence": [{"caption": "\\WarningsOff[]", "snippet": "\\WarningsOff[$1]", "meta": "silence-cmd", "score": 0.00014933999190577243}, {"caption": "\\WarningFilter{}{}", "snippet": "\\WarningFilter{$1}{$2}", "meta": "silence-cmd", "score": 0.0010293824370507024}], "numprint": [{"caption": "\\textcelsius", "snippet": "\\textcelsius", "meta": "numprint-cmd", "score": 0.00012244782670334462}, {"caption": "\\pm", "snippet": "\\pm", "meta": "numprint-cmd", "score": 0.15663535405975132}, {"caption": "\\npdecimalsign{}", "snippet": "\\npdecimalsign{$1}", "meta": "numprint-cmd", "score": 8.401009062000455e-06}, {"caption": "\\npthousandsep{}", "snippet": "\\npthousandsep{$1}", "meta": "numprint-cmd", "score": 8.401009062000455e-06}, {"caption": "\\np{}", "snippet": "\\np{$1}", "meta": "numprint-cmd", "score": 0.0001782233963311367}, {"caption": "\\np", "snippet": "\\np", "meta": "numprint-cmd", "score": 0.0001782233963311367}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "numprint-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "numprint-cmd", "score": 0.2864294797053033}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "numprint-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "numprint-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "numprint-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "numprint-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "numprint-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "numprint-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "numprint-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "numprint-cmd", "score": 0.018615449342361392}], "srcltx": [{"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "srcltx-cmd", "score": 0.2659628337907604}, {"caption": "\\input{}", "snippet": "\\input{$1}", "meta": "srcltx-cmd", "score": 0.4966021927742672}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "srcltx-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "srcltx-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "srcltx-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "srcltx-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "srcltx-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "srcltx-cmd", "score": 0.0018957469739775527}], "ctable": [{"caption": "\\tmark[]", "snippet": "\\tmark[$1]", "meta": "ctable-cmd", "score": 0.004423748442334348}, {"caption": "\\ctable[]{}{}{}", "snippet": "\\ctable[$1]{$2}{$3}{$4}", "meta": "ctable-cmd", "score": 0.0007377841391165772}, {"caption": "\\let", "snippet": "\\let", "meta": "ctable-cmd", "score": 0.03789745970461662}, {"caption": "\\write", "snippet": "\\write", "meta": "ctable-cmd", "score": 0.0008038857295393196}, {"caption": "\\tabularxcolumn[]{}", "snippet": "\\tabularxcolumn[$1]{$2}", "meta": "ctable-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularxcolumn", "snippet": "\\tabularxcolumn", "meta": "ctable-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularx{}{}", "snippet": "\\tabularx{$1}{$2}", "meta": "ctable-cmd", "score": 0.0005861357565780464}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ctable-cmd", "score": 0.014532521139459619}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "ctable-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "ctable-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "ctable-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "ctable-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "ctable-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "ctable-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "ctable-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "ctable-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "ctable-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ctable-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "ctable-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "ctable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "ctable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "ctable-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "ctable-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "ctable-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "ctable-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "ctable-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "ctable-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "ctable-cmd", "score": 0.0018957469739775527}, {"caption": "\\specialrule{}{}{}", "snippet": "\\specialrule{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.004974385202605165}, {"caption": "\\cmidrule", "snippet": "\\cmidrule", "meta": "ctable-cmd", "score": 0.01894952272365088}, {"caption": "\\cmidrule{}", "snippet": "\\cmidrule{$1}", "meta": "ctable-cmd", "score": 0.01894952272365088}, {"caption": "\\bottomrule", "snippet": "\\bottomrule", "meta": "ctable-cmd", "score": 0.04533364657852219}, {"caption": "\\midrule", "snippet": "\\midrule", "meta": "ctable-cmd", "score": 0.07098077735912875}, {"caption": "\\addlinespace", "snippet": "\\addlinespace", "meta": "ctable-cmd", "score": 0.005865460617491447}, {"caption": "\\addlinespace[]", "snippet": "\\addlinespace[$1]", "meta": "ctable-cmd", "score": 0.005865460617491447}, {"caption": "\\toprule", "snippet": "\\toprule", "meta": "ctable-cmd", "score": 0.059857788139528495}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "ctable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "ctable-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "ctable-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ctable-cmd", "score": 0.008565354665444157}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "ctable-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "ctable-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ctable-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "ctable-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ctable-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "ctable-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "ctable-cmd", "score": 0.018615449342361392}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "ctable-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "ctable-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "ctable-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "ctable-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "ctable-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "ctable-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "ctable-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "ctable-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "ctable-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "ctable-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "ctable-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "ctable-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "ctable-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "ctable-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "ctable-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "ctable-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "ctable-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ctable-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "ctable-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "ctable-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "ctable-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "ctable-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "ctable-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "ctable-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "ctable-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "ctable-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "ctable-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ctable-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "ctable-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "ctable-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "ctable-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "ctable-cmd", "score": 0.2864294797053033}], "bbding": [{"caption": "\\HandRight", "snippet": "\\HandRight", "meta": "bbding-cmd", "score": 9.986169155719329e-05}, {"caption": "\\XSolidBrush", "snippet": "\\XSolidBrush", "meta": "bbding-cmd", "score": 0.0003502234425563509}, {"caption": "\\Checkmark", "snippet": "\\Checkmark", "meta": "bbding-cmd", "score": 0.0010506703276690528}], "endfloat": [{"caption": "\\DeclareDelayedFloatFlavor{}{}", "snippet": "\\DeclareDelayedFloatFlavor{$1}{$2}", "meta": "endfloat-cmd", "score": 0.00012872796177294446}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "endfloat-cmd", "score": 0.00037306820619479756}], "centernot": [{"caption": "\\centernot", "snippet": "\\centernot", "meta": "centernot-cmd", "score": 0.0002513707969474898}], "tikzpagenodes": [{"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpagenodes-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpagenodes-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikzpagenodes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikzpagenodes-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikzpagenodes-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikzpagenodes-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikzpagenodes-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikzpagenodes-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzpagenodes-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikzpagenodes-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpagenodes-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikzpagenodes-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikzpagenodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikzpagenodes-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikzpagenodes-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikzpagenodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikzpagenodes-cmd", "score": 0.004719094298848707}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzpagenodes-cmd", "score": 0.00530510025314411}, {"caption": "\\checkoddpage", "snippet": "\\checkoddpage", "meta": "tikzpagenodes-cmd", "score": 0.00028672585452906425}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikzpagenodes-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikzpagenodes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikzpagenodes-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikzpagenodes-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikzpagenodes-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikzpagenodes-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzpagenodes-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikzpagenodes-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzpagenodes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikzpagenodes-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikzpagenodes-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikzpagenodes-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikzpagenodes-cmd", "score": 0.2864294797053033}], "xargs": [{"caption": "\\newcommandx{}[][]{}", "snippet": "\\newcommandx{$1}[$2][$3]{$4}", "meta": "xargs-cmd", "score": 0.0001110821063389004}], "morefloats": [{"caption": "\\empty", "snippet": "\\empty", "meta": "morefloats-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "morefloats-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "morefloats-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "morefloats-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "morefloats-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "morefloats-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "morefloats-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "morefloats-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "morefloats-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "morefloats-cmd", "score": 0.021170869458413965}], "background": [{"caption": "\\BgThispage", "snippet": "\\BgThispage", "meta": "background-cmd", "score": 0.0003956357273698423}, {"caption": "\\backgroundsetup{}", "snippet": "\\backgroundsetup{$1}", "meta": "background-cmd", "score": 0.0004910777123492879}], "bibunits": [{"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "bibunits-cmd", "score": 0.2659628337907604}], "moresize": [{"caption": "\\Huge", "snippet": "\\Huge", "meta": "moresize-cmd", "score": 0.04725806985998919}], "pgfpages": [{"caption": "\\pgfpagesphysicalpageoptions{}", "snippet": "\\pgfpagesphysicalpageoptions{$1}", "meta": "pgfpages-cmd", "score": 0.00045967325420052095}, {"caption": "\\pgfpageslogicalpageoptions{}{}", "snippet": "\\pgfpageslogicalpageoptions{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.00045967325420052095}, {"caption": "\\pgfpageoptionborder{}", "snippet": "\\pgfpageoptionborder{$1}", "meta": "pgfpages-cmd", "score": 0.0009193465084010419}, {"caption": "\\pgfpageoptionborder", "snippet": "\\pgfpageoptionborder", "meta": "pgfpages-cmd", "score": 0.0009193465084010419}, {"caption": "\\pgfpagesdeclarelayout{}{}{}", "snippet": "\\pgfpagesdeclarelayout{$1}{$2}{$3}", "meta": "pgfpages-cmd", "score": 0.00045967325420052095}, {"caption": "\\pgfpagesuselayout{}", "snippet": "\\pgfpagesuselayout{$1}", "meta": "pgfpages-cmd", "score": 0.0006090132461062934}, {"caption": "\\pgfpagesuselayout{}[]", "snippet": "\\pgfpagesuselayout{$1}[$2]", "meta": "pgfpages-cmd", "score": 0.0006090132461062934}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "pgfpages-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "pgfpages-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "pgfpages-cmd", "score": 0.028955796305270766}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfpages-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfpages-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfpages-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfpages-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfpages-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfpages-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfpages-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfpages-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfpages-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfpages-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfpages-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfpages-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfpages-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfpages-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfpages-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfpages-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfpages-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfpages-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfpages-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfpages-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfpages-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfpages-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfpages-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfpages-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfpages-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfpages-cmd", "score": 0.2864294797053033}], "ctex": [{"caption": "\\CTeX", "snippet": "\\CTeX", "meta": "ctex-cmd", "score": 0.0005884706823906032}, {"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "ctex-cmd", "score": 0.04598628699063736}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "ctex-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "ctex-cmd", "score": 0.2864294797053033}], "algcompatible": [{"caption": "\\algrenewcommand", "snippet": "\\algrenewcommand", "meta": "algcompatible-cmd", "score": 0.0019861803661869416}, {"caption": "\\Statex", "snippet": "\\Statex", "meta": "algcompatible-cmd", "score": 0.008622777195102994}, {"caption": "\\BState{}", "snippet": "\\BState{$1}", "meta": "algcompatible-cmd", "score": 0.0008685861525307122}, {"caption": "\\BState", "snippet": "\\BState", "meta": "algcompatible-cmd", "score": 0.0008685861525307122}, {"caption": "\\algloopdefx{}[][]{}", "snippet": "\\algloopdefx{$1}[$2][$3]{$4}", "meta": "algcompatible-cmd", "score": 0.00025315185701145097}, {"caption": "\\algnewcommand", "snippet": "\\algnewcommand", "meta": "algcompatible-cmd", "score": 0.0030209395012065327}, {"caption": "\\algnewcommand{}[]{}", "snippet": "\\algnewcommand{$1}[$2]{$3}", "meta": "algcompatible-cmd", "score": 0.0030209395012065327}, {"caption": "\\Comment{}", "snippet": "\\Comment{$1}", "meta": "algcompatible-cmd", "score": 0.005178604573219454}, {"caption": "\\algblockdefx{}{}[]", "snippet": "\\algblockdefx{$1}{$2}[$3]", "meta": "algcompatible-cmd", "score": 0.00025315185701145097}, {"caption": "\\algrenewtext{}{}", "snippet": "\\algrenewtext{$1}{$2}", "meta": "algcompatible-cmd", "score": 0.0024415580558825975}, {"caption": "\\algrenewtext{}[]{}", "snippet": "\\algrenewtext{$1}[$2]{$3}", "meta": "algcompatible-cmd", "score": 0.0024415580558825975}, {"caption": "\\algblock{}{}", "snippet": "\\algblock{$1}{$2}", "meta": "algcompatible-cmd", "score": 0.0007916858220314837}, {"caption": "\\csname", "snippet": "\\csname", "meta": "algcompatible-cmd", "score": 0.008565354665444157}, {"caption": "\\algdef{}[]{}{}{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "algcompatible-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}{}[]{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}", "meta": "algcompatible-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}[]{}", "snippet": "\\algdef{$1}[$2]{$3}[$4]{$5}", "meta": "algcompatible-cmd", "score": 0.0003102486920966127}, {"caption": "\\algtext{}", "snippet": "\\algtext{$1}", "meta": "algcompatible-cmd", "score": 0.0005463612015579842}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "algcompatible-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "algcompatible-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "algcompatible-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "algcompatible-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "algcompatible-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "algcompatible-cmd", "score": 0.0018957469739775527}], "draftwatermark": [{"caption": "\\SetWatermarkScale{}", "snippet": "\\SetWatermarkScale{$1}", "meta": "draftwatermark-cmd", "score": 0.0013776850432469145}, {"caption": "\\SetWatermarkText{}", "snippet": "\\SetWatermarkText{$1}", "meta": "draftwatermark-cmd", "score": 0.0017209596079747669}, {"caption": "\\SetWatermarkColor[]{}", "snippet": "\\SetWatermarkColor[$1]{$2}", "meta": "draftwatermark-cmd", "score": 0.0007061648188687239}, {"caption": "\\SetWatermarkFontSize{}", "snippet": "\\SetWatermarkFontSize{$1}", "meta": "draftwatermark-cmd", "score": 0.0005747853176838451}, {"caption": "\\SetWatermarkLightness{}", "snippet": "\\SetWatermarkLightness{$1}", "meta": "draftwatermark-cmd", "score": 0.0005747853176838451}, {"caption": "\\SetWatermarkAngle{}", "snippet": "\\SetWatermarkAngle{$1}", "meta": "draftwatermark-cmd", "score": 0.0005747853176838451}, {"caption": "\\csname", "snippet": "\\csname", "meta": "draftwatermark-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "draftwatermark-cmd", "score": 0.00037306820619479756}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "draftwatermark-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "draftwatermark-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "draftwatermark-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "draftwatermark-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "draftwatermark-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "draftwatermark-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "draftwatermark-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "draftwatermark-cmd", "score": 0.2864294797053033}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "draftwatermark-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "draftwatermark-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "draftwatermark-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "draftwatermark-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "draftwatermark-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "draftwatermark-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "draftwatermark-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "draftwatermark-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "draftwatermark-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "draftwatermark-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "draftwatermark-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "draftwatermark-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "draftwatermark-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "draftwatermark-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "draftwatermark-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "draftwatermark-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "draftwatermark-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "draftwatermark-cmd", "score": 0.004719094298848707}], "eqparbox": [{"caption": "\\eqparbox{}{}", "snippet": "\\eqparbox{$1}{$2}", "meta": "eqparbox-cmd", "score": 2.9423534119530166e-05}, {"caption": "\\item", "snippet": "\\item", "meta": "eqparbox-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "eqparbox-cmd", "score": 3.800886892251021}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "eqparbox-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "eqparbox-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "eqparbox-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "eqparbox-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "eqparbox-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "eqparbox-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "eqparbox-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "eqparbox-cmd", "score": 0.018615449342361392}, {"caption": "\\csname", "snippet": "\\csname", "meta": "eqparbox-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "eqparbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "eqparbox-cmd", "score": 0.021170869458413965}], "nowidow": [{"caption": "\\empty", "snippet": "\\empty", "meta": "nowidow-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "nowidow-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nowidow-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "nowidow-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "nowidow-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nowidow-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nowidow-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "nowidow-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "nowidow-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "nowidow-cmd", "score": 0.021170869458413965}], "stackrel": [{"caption": "\\stackrel{}{}", "snippet": "\\stackrel{$1}{$2}", "meta": "stackrel-cmd", "score": 0.009911875742973681}, {"caption": "\\csname", "snippet": "\\csname", "meta": "stackrel-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "stackrel-cmd", "score": 0.002958865219480927}], "threeparttablex": [{"caption": "\\item", "snippet": "\\item", "meta": "threeparttablex-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "threeparttablex-cmd", "score": 3.800886892251021}, {"caption": "\\insertTableNotes", "snippet": "\\insertTableNotes", "meta": "threeparttablex-cmd", "score": 4.002553629215439e-05}, {"caption": "\\tnotex{}", "snippet": "\\tnotex{$1}", "meta": "threeparttablex-cmd", "score": 0.0021491972748178554}, {"caption": "\\csname", "snippet": "\\csname", "meta": "threeparttablex-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "threeparttablex-cmd", "score": 0.008565354665444157}, {"caption": "\\item", "snippet": "\\item", "meta": "threeparttablex-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "threeparttablex-cmd", "score": 3.800886892251021}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "threeparttablex-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "threeparttablex-cmd", "score": 0.021170869458413965}], "mathdesign": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mathdesign-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mathdesign-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mathdesign-cmd", "score": 0.021170869458413965}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "mathdesign-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "mathdesign-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "mathdesign-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "mathdesign-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "mathdesign-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "mathdesign-cmd", "score": 0.0018957469739775527}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "mathdesign-cmd", "score": 0.00037306820619479756}], "pst-node": [{"caption": "\\green", "snippet": "\\green", "meta": "pst-node-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pst-node-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pst-node-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pst-node-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pst-node-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pst-node-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pst-node-cmd", "score": 0.006520475264573554}], "varwidth": [{"caption": "\\par", "snippet": "\\par", "meta": "varwidth-cmd", "score": 0.413853376001159}], "schemabloc": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "schemabloc-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "schemabloc-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "schemabloc-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "schemabloc-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "schemabloc-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "schemabloc-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "schemabloc-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "schemabloc-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "schemabloc-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "schemabloc-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "schemabloc-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "schemabloc-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "schemabloc-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "schemabloc-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "schemabloc-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "schemabloc-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "schemabloc-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "schemabloc-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "schemabloc-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "schemabloc-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "schemabloc-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "schemabloc-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "schemabloc-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "schemabloc-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "schemabloc-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "schemabloc-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "schemabloc-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "schemabloc-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "schemabloc-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "schemabloc-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "schemabloc-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "schemabloc-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "schemabloc-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "schemabloc-cmd", "score": 0.2864294797053033}], "bigints": [{"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "bigints-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "bigints-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "bigints-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "bigints-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "bigints-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "bigints-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "bigints-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "bigints-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "bigints-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "bigints-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "bigints-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "bigints-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "bigints-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "bigints-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "bigints-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "bigints-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "bigints-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "bigints-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "bigints-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "bigints-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "bigints-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "bigints-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "bigints-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "bigints-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "bigints-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "bigints-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "bigints-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "bigints-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "bigints-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "bigints-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "bigints-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "bigints-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "bigints-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "bigints-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "bigints-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "bigints-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "bigints-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "bigints-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "bigints-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "bigints-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "bigints-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "bigints-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "bigints-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "bigints-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "bigints-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "bigints-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "bigints-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "bigints-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "bigints-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "bigints-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "bigints-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "bigints-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "bigints-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "bigints-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "bigints-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "bigints-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "bigints-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "bigints-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "bigints-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "bigints-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "bigints-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "bigints-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "bigints-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "bigints-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "bigints-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "bigints-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "bigints-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "bigints-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "bigints-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "bigints-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "bigints-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "bigints-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "bigints-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "bigints-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "bigints-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "bigints-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "bigints-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "bigints-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "bigints-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "bigints-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "bigints-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "bigints-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "bigints-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "bigints-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "bigints-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "bigints-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "bigints-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "bigints-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "bigints-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "bigints-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "bigints-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "bigints-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "bigints-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "bigints-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "bigints-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "bigints-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "bigints-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "bigints-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "bigints-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "bigints-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "bigints-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "bigints-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "bigints-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "bigints-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "bigints-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "bigints-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "bigints-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "bigints-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "bigints-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "bigints-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "bigints-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "bigints-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "bigints-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "bigints-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "bigints-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "bigints-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "bigints-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "bigints-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "bigints-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "bigints-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "bigints-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "bigints-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "bigints-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "bigints-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "bigints-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "bigints-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "bigints-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "bigints-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "bigints-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "bigints-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "bigints-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "bigints-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "bigints-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "bigints-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "bigints-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "bigints-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "bigints-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "bigints-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "bigints-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "bigints-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "bigints-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "bigints-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "bigints-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "bigints-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "bigints-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "bigints-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "bigints-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "bigints-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "bigints-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bigints-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "bigints-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "bigints-cmd", "score": 0.0063276692758974925}], "classicthesis": [{"caption": "\\marginpar{}", "snippet": "\\marginpar{$1}", "meta": "classicthesis-cmd", "score": 0.003400158497921723}, {"caption": "\\marginpar", "snippet": "\\marginpar", "meta": "classicthesis-cmd", "score": 0.003400158497921723}, {"caption": "\\cftsecleader", "snippet": "\\cftsecleader", "meta": "classicthesis-cmd", "score": 0.0011340882025681251}, {"caption": "\\cftsubsecleader", "snippet": "\\cftsubsecleader", "meta": "classicthesis-cmd", "score": 1.0644172549700836e-05}, {"caption": "\\spacedlowsmallcaps{}", "snippet": "\\spacedlowsmallcaps{$1}", "meta": "classicthesis-cmd", "score": 0.002677188251799468}, {"caption": "\\sectionmark", "snippet": "\\sectionmark", "meta": "classicthesis-cmd", "score": 0.005008938879210868}, {"caption": "\\chaptermark", "snippet": "\\chaptermark", "meta": "classicthesis-cmd", "score": 0.005924520024686584}, {"caption": "\\chaptermark{}", "snippet": "\\chaptermark{$1}", "meta": "classicthesis-cmd", "score": 0.005924520024686584}, {"caption": "\\part{}", "snippet": "\\part{$1}", "meta": "classicthesis-cmd", "score": 0.022180129487444723}, {"caption": "\\tocEntry{}", "snippet": "\\tocEntry{$1}", "meta": "classicthesis-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\graffito{}", "snippet": "\\graffito{$1}", "meta": "classicthesis-cmd", "score": 1.1006799670632527e-05}, {"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "classicthesis-cmd", "score": 0.422097569591803}, {"caption": "\\spacedallcaps{}", "snippet": "\\spacedallcaps{$1}", "meta": "classicthesis-cmd", "score": 0.0015281000475958944}, {"caption": "\\cftchapleader", "snippet": "\\cftchapleader", "meta": "classicthesis-cmd", "score": 1.0644172549700836e-05}, {"caption": "\\myVersion", "snippet": "\\myVersion", "meta": "classicthesis-cmd", "score": 0.00018029288638573757}, {"caption": "\\ctparttext{}", "snippet": "\\ctparttext{$1}", "meta": "classicthesis-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "classicthesis-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "classicthesis-cmd", "score": 0.1789117552185788}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "classicthesis-cmd", "score": 0.00037306820619479756}, {"caption": "\\specialrule{}{}{}", "snippet": "\\specialrule{$1}{$2}{$3}", "meta": "classicthesis-cmd", "score": 0.004974385202605165}, {"caption": "\\cmidrule", "snippet": "\\cmidrule", "meta": "classicthesis-cmd", "score": 0.01894952272365088}, {"caption": "\\cmidrule{}", "snippet": "\\cmidrule{$1}", "meta": "classicthesis-cmd", "score": 0.01894952272365088}, {"caption": "\\bottomrule", "snippet": "\\bottomrule", "meta": "classicthesis-cmd", "score": 0.04533364657852219}, {"caption": "\\midrule", "snippet": "\\midrule", "meta": "classicthesis-cmd", "score": 0.07098077735912875}, {"caption": "\\addlinespace", "snippet": "\\addlinespace", "meta": "classicthesis-cmd", "score": 0.005865460617491447}, {"caption": "\\addlinespace[]", "snippet": "\\addlinespace[$1]", "meta": "classicthesis-cmd", "score": 0.005865460617491447}, {"caption": "\\toprule", "snippet": "\\toprule", "meta": "classicthesis-cmd", "score": 0.059857788139528495}, {"caption": "\\titleclass{}{}[]", "snippet": "\\titleclass{$1}{$2}[$3]", "meta": "classicthesis-cmd", "score": 0.00028979763314974667}, {"caption": "\\titlelabel{}", "snippet": "\\titlelabel{$1}", "meta": "classicthesis-cmd", "score": 6.40387839367932e-06}, {"caption": "\\thetitle", "snippet": "\\thetitle", "meta": "classicthesis-cmd", "score": 0.0015531478302713473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "classicthesis-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "classicthesis-cmd", "score": 0.021170869458413965}, {"caption": "\\titleformat{}{}{}{}{}[]", "snippet": "\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]", "meta": "classicthesis-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}[]{}{}{}{}", "snippet": "\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "classicthesis-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}{}", "snippet": "\\titleformat{$1}{$2}", "meta": "classicthesis-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}{}{}{}{}", "snippet": "\\titleformat{$1}{$2}{$3}{$4}{$5}", "meta": "classicthesis-cmd", "score": 0.03475519439740096}, {"caption": "\\titlespacing{}{}{}{}", "snippet": "\\titlespacing{$1}{$2}{$3}{$4}", "meta": "classicthesis-cmd", "score": 0.023062744385192156}, {"caption": "\\markboth{}{}", "snippet": "\\markboth{$1}{$2}", "meta": "classicthesis-cmd", "score": 0.038323601301945065}, {"caption": "\\markboth{}", "snippet": "\\markboth{$1}", "meta": "classicthesis-cmd", "score": 0.038323601301945065}, {"caption": "\\markright{}", "snippet": "\\markright{$1}", "meta": "classicthesis-cmd", "score": 0.007138622674767024}, {"caption": "\\markright{}{}", "snippet": "\\markright{$1}{$2}", "meta": "classicthesis-cmd", "score": 0.007138622674767024}, {"caption": "\\filleft", "snippet": "\\filleft", "meta": "classicthesis-cmd", "score": 7.959989906732799e-05}, {"caption": "\\filcenter", "snippet": "\\filcenter", "meta": "classicthesis-cmd", "score": 0.0004835660211260246}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "classicthesis-cmd", "score": 0.2253056071787701}, {"caption": "\\cleardoublepage", "snippet": "\\cleardoublepage", "meta": "classicthesis-cmd", "score": 0.044016804142963585}, {"caption": "\\csname", "snippet": "\\csname", "meta": "classicthesis-cmd", "score": 0.008565354665444157}, {"caption": "\\chaptertitlename", "snippet": "\\chaptertitlename", "meta": "classicthesis-cmd", "score": 0.0016985007766926272}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "classicthesis-cmd", "score": 0.3277033727934986}, {"caption": "\\filright", "snippet": "\\filright", "meta": "classicthesis-cmd", "score": 7.959989906732799e-05}, {"caption": "\\titlerule", "snippet": "\\titlerule", "meta": "classicthesis-cmd", "score": 0.019273712561461216}, {"caption": "\\titlerule[]{}", "snippet": "\\titlerule[$1]{$2}", "meta": "classicthesis-cmd", "score": 0.019273712561461216}, {"caption": "\\addtokomafont{}{}", "snippet": "\\addtokomafont{$1}{$2}", "meta": "classicthesis-cmd", "score": 0.0008555564394100388}, {"caption": "\\setkomafont{}{}", "snippet": "\\setkomafont{$1}{$2}", "meta": "classicthesis-cmd", "score": 0.012985816912639263}, {"caption": "\\KOMAoptions{}", "snippet": "\\KOMAoptions{$1}", "meta": "classicthesis-cmd", "score": 0.000396664302361659}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "classicthesis-cmd", "score": 2.341195220791228}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "classicthesis-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "classicthesis-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "classicthesis-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "classicthesis-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "classicthesis-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "classicthesis-cmd", "score": 0.0018957469739775527}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "classicthesis-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "classicthesis-cmd", "score": 0.021170869458413965}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "classicthesis-cmd", "score": 0.00530510025314411}, {"caption": "\\lsstyle", "snippet": "\\lsstyle", "meta": "classicthesis-cmd", "score": 0.0023367519914345774}, {"caption": "\\space", "snippet": "\\space", "meta": "classicthesis-cmd", "score": 0.023010789853665694}, {"caption": "\\DisableLigatures[]{}", "snippet": "\\DisableLigatures[$1]{$2}", "meta": "classicthesis-cmd", "score": 0.0009805246614299932}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "classicthesis-cmd", "score": 0.00021116765384691477}], "expl3": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "expl3-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "expl3-cmd", "score": 0.2864294797053033}], "pst-plot": [{"caption": "\\green", "snippet": "\\green", "meta": "pst-plot-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pst-plot-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pst-plot-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pst-plot-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pst-plot-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pst-plot-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pst-plot-cmd", "score": 0.006520475264573554}], "chemarrow": [{"caption": "\\chemarrow", "snippet": "\\chemarrow", "meta": "chemarrow-cmd", "score": 0.0005176077206367611}], "prettyref": [{"caption": "\\newrefformat{}{}", "snippet": "\\newrefformat{$1}{$2}", "meta": "prettyref-cmd", "score": 0.001373625900102228}, {"caption": "\\prettyref{}", "snippet": "\\prettyref{$1}", "meta": "prettyref-cmd", "score": 0.005783541047730358}], "versions": [{"caption": "\\includeversion{}", "snippet": "\\includeversion{$1}", "meta": "versions-cmd", "score": 0.0028410409433993543}, {"caption": "\\excludeversion{}", "snippet": "\\excludeversion{$1}", "meta": "versions-cmd", "score": 0.001742562336270228}, {"caption": "\\processifversion{}{}", "snippet": "\\processifversion{$1}{$2}", "meta": "versions-cmd", "score": 0.0022991412707353805}], "contour": [{"caption": "\\contour{}{}", "snippet": "\\contour{$1}{$2}", "meta": "contour-cmd", "score": 0.0008245159401597211}, {"caption": "\\contourlength{}", "snippet": "\\contourlength{$1}", "meta": "contour-cmd", "score": 8.130187059343861e-05}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "contour-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "contour-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "contour-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "contour-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "contour-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "contour-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "contour-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "contour-cmd", "score": 0.2864294797053033}, {"caption": "\\csname", "snippet": "\\csname", "meta": "contour-cmd", "score": 0.008565354665444157}], "xintexpr": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xintexpr-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xintexpr-cmd", "score": 0.021170869458413965}], "tocstyle": [{"caption": "\\usetocstyle{}", "snippet": "\\usetocstyle{$1}", "meta": "tocstyle-cmd", "score": 3.2405622997778076e-06}], "bigdelim": [{"caption": "\\multirow{}{}{}", "snippet": "\\multirow{$1}{$2}{$3}", "meta": "bigdelim-cmd", "score": 0.07525389638751734}, {"caption": "\\multirow{}[]{}{}", "snippet": "\\multirow{$1}[$2]{$3}{$4}", "meta": "bigdelim-cmd", "score": 0.07525389638751734}], "eulervm": [{"caption": "\\big", "snippet": "\\big", "meta": "eulervm-cmd", "score": 0.05613164277964739}], "xr": [{"caption": "\\externaldocument{}", "snippet": "\\externaldocument{$1}", "meta": "xr-cmd", "score": 0.0008648763879096798}], "yhmath": [{"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "yhmath-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "yhmath-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "yhmath-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "yhmath-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "yhmath-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "yhmath-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "yhmath-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "yhmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "yhmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "yhmath-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "yhmath-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "yhmath-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "yhmath-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "yhmath-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "yhmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "yhmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "yhmath-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "yhmath-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "yhmath-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "yhmath-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "yhmath-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "yhmath-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "yhmath-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "yhmath-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "yhmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "yhmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "yhmath-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "yhmath-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "yhmath-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "yhmath-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "yhmath-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "yhmath-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "yhmath-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "yhmath-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "yhmath-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "yhmath-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "yhmath-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "yhmath-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "yhmath-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "yhmath-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "yhmath-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "yhmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "yhmath-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "yhmath-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "yhmath-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "yhmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "yhmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "yhmath-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "yhmath-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "yhmath-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "yhmath-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "yhmath-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "yhmath-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "yhmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "yhmath-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "yhmath-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "yhmath-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "yhmath-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "yhmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "yhmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "yhmath-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "yhmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "yhmath-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "yhmath-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "yhmath-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "yhmath-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "yhmath-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "yhmath-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "yhmath-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "yhmath-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "yhmath-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "yhmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "yhmath-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "yhmath-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "yhmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "yhmath-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "yhmath-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "yhmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "yhmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "yhmath-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "yhmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "yhmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "yhmath-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "yhmath-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "yhmath-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "yhmath-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "yhmath-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "yhmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "yhmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "yhmath-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "yhmath-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "yhmath-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "yhmath-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "yhmath-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "yhmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "yhmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "yhmath-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "yhmath-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "yhmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "yhmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "yhmath-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "yhmath-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "yhmath-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "yhmath-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "yhmath-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "yhmath-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "yhmath-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "yhmath-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "yhmath-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "yhmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "yhmath-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "yhmath-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "yhmath-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "yhmath-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "yhmath-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "yhmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "yhmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "yhmath-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "yhmath-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "yhmath-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "yhmath-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "yhmath-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "yhmath-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "yhmath-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "yhmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "yhmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "yhmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "yhmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "yhmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "yhmath-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "yhmath-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "yhmath-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "yhmath-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "yhmath-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "yhmath-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "yhmath-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "yhmath-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "yhmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "yhmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "yhmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "yhmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "yhmath-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "yhmath-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "yhmath-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "yhmath-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "yhmath-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "yhmath-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "yhmath-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "yhmath-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "yhmath-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "yhmath-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "yhmath-cmd", "score": 0.0063276692758974925}], "XCharter": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "XCharter-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "XCharter-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "XCharter-cmd", "score": 0.021170869458413965}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "XCharter-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "XCharter-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "XCharter-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "XCharter-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "XCharter-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "XCharter-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "XCharter-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "XCharter-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "XCharter-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "XCharter-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "XCharter-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "XCharter-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "XCharter-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "XCharter-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "XCharter-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "XCharter-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "XCharter-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "XCharter-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "XCharter-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "XCharter-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "XCharter-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "XCharter-cmd", "score": 0.008565354665444157}], "tikz-feynman": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-feynman-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-feynman-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikz-feynman-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikz-feynman-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikz-feynman-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikz-feynman-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-feynman-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikz-feynman-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-feynman-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikz-feynman-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-feynman-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-feynman-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikz-feynman-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-feynman-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-feynman-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-feynman-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-feynman-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-feynman-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-feynman-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikz-feynman-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-feynman-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-feynman-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikz-feynman-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikz-feynman-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikz-feynman-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-feynman-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikz-feynman-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-feynman-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikz-feynman-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikz-feynman-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikz-feynman-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikz-feynman-cmd", "score": 0.2864294797053033}], "easylist": [{"caption": "\\ListProperties", "snippet": "\\ListProperties", "meta": "easylist-cmd", "score": 5.7747123038330224e-05}], "hologo": [{"caption": "\\hologo{}", "snippet": "\\hologo{$1}", "meta": "hologo-cmd", "score": 0.00028086100750460613}], "cases": [{"caption": "\\theequation", "snippet": "\\theequation", "meta": "cases-cmd", "score": 0.002995924112493351}], "xint": [{"caption": "\\xintSgnFork{}", "snippet": "\\xintSgnFork{$1}", "meta": "xint-cmd", "score": 0.0005720629946669665}, {"caption": "\\xintCmp{}{}", "snippet": "\\xintCmp{$1}{$2}", "meta": "xint-cmd", "score": 0.0002860314973334833}, {"caption": "\\xintOdd{}", "snippet": "\\xintOdd{$1}", "meta": "xint-cmd", "score": 0.0002860314973334833}, {"caption": "\\xintGeq", "snippet": "\\xintGeq", "meta": "xint-cmd", "score": 0.0002860314973334833}], "inputenx": [{"caption": "\\inputencoding{}", "snippet": "\\inputencoding{$1}", "meta": "inputenx-cmd", "score": 0.0002447047447770061}], "vwcol": [{"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "vwcol-cmd", "score": 0.04598628699063736}, {"caption": "\\csname", "snippet": "\\csname", "meta": "vwcol-cmd", "score": 0.008565354665444157}, {"caption": "\\justifying", "snippet": "\\justifying", "meta": "vwcol-cmd", "score": 0.010373702256548788}, {"caption": "\\justifying{}", "snippet": "\\justifying{$1}", "meta": "vwcol-cmd", "score": 0.010373702256548788}, {"caption": "\\RaggedRight", "snippet": "\\RaggedRight", "meta": "vwcol-cmd", "score": 0.001021021782267457}, {"caption": "\\Centering", "snippet": "\\Centering", "meta": "vwcol-cmd", "score": 0.00037395241488843035}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "vwcol-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "vwcol-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "vwcol-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "vwcol-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "vwcol-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "vwcol-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "vwcol-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "vwcol-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "vwcol-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "vwcol-cmd", "score": 0.028955796305270766}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "vwcol-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "vwcol-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "vwcol-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "vwcol-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "vwcol-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "vwcol-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "vwcol-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "vwcol-cmd", "score": 0.2864294797053033}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "vwcol-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "vwcol-cmd", "score": 0.021170869458413965}], "multimedia": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "multimedia-cmd", "score": 0.00037306820619479756}], "sgame": [{"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "sgame-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "sgame-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "sgame-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "sgame-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "sgame-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "sgame-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "sgame-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "sgame-cmd", "score": 0.2864294797053033}], "bussproofs": [{"caption": "\\makeatletter", "snippet": "\\makeatletter", "meta": "bussproofs-cmd", "score": 0.041979363643201636}, {"caption": "\\makeatother", "snippet": "\\makeatother", "meta": "bussproofs-cmd", "score": 0.03923442255397878}], "titlepic": [{"caption": "\\titlepic{}", "snippet": "\\titlepic{$1}", "meta": "titlepic-cmd", "score": 0.00020896323441399082}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "titlepic-cmd", "score": 0.7504160124360846}], "paracol": [{"caption": "\\switchcolumn", "snippet": "\\switchcolumn", "meta": "paracol-cmd", "score": 0.0008273060639466222}, {"caption": "\\csname", "snippet": "\\csname", "meta": "paracol-cmd", "score": 0.008565354665444157}], "polyglossia": [{"caption": "\\markboth{}{}", "snippet": "\\markboth{$1}{$2}", "meta": "polyglossia-cmd", "score": 0.038323601301945065}, {"caption": "\\markboth{}", "snippet": "\\markboth{$1}", "meta": "polyglossia-cmd", "score": 0.038323601301945065}, {"caption": "\\normalfont", "snippet": "\\normalfont", "meta": "polyglossia-cmd", "score": 0.06871177093091137}, {"caption": "\\normalfont{}", "snippet": "\\normalfont{$1}", "meta": "polyglossia-cmd", "score": 0.06871177093091137}, {"caption": "\\setdefaultlanguage{}", "snippet": "\\setdefaultlanguage{$1}", "meta": "polyglossia-cmd", "score": 0.00021116765384691477}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "polyglossia-cmd", "score": 0.00021116765384691477}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "polyglossia-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "polyglossia-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "polyglossia-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "polyglossia-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "polyglossia-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "polyglossia-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "polyglossia-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "polyglossia-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "polyglossia-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "polyglossia-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "polyglossia-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "polyglossia-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "polyglossia-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "polyglossia-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "polyglossia-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "polyglossia-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "polyglossia-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "polyglossia-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "polyglossia-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "polyglossia-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "polyglossia-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "polyglossia-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "polyglossia-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "polyglossia-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "polyglossia-cmd", "score": 0.2864294797053033}], "zref-user": [{"caption": "\\zlabel{}", "snippet": "\\zlabel{$1}", "meta": "zref-user-cmd", "score": 0.0005277905480209891}, {"caption": "\\zref", "snippet": "\\zref", "meta": "zref-user-cmd", "score": 0.002193637536912482}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-user-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-user-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-user-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "zref-user-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "zref-user-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-user-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-user-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "zref-user-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-user-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-user-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "zref-user-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "zref-user-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-user-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-user-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-user-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "zref-user-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-user-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-user-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-user-cmd", "score": 0.002958865219480927}], "zref-abspage": [{"caption": "\\empty", "snippet": "\\empty", "meta": "zref-abspage-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "zref-abspage-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "zref-abspage-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "zref-abspage-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "zref-abspage-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-abspage-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-abspage-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "zref-abspage-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "zref-abspage-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "zref-abspage-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-abspage-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "zref-abspage-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "zref-abspage-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-abspage-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-abspage-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "zref-abspage-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-abspage-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-abspage-cmd", "score": 0.002958865219480927}], "quotchap": [{"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "quotchap-cmd", "score": 0.422097569591803}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "quotchap-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "quotchap-cmd", "score": 0.2864294797053033}, {"caption": "\\qauthor{}", "snippet": "\\qauthor{$1}", "meta": "quotchap-cmd", "score": 0.002335082759143631}], "misccorr": [{"caption": "\\subsection{}", "snippet": "\\subsection{$1}", "meta": "misccorr-cmd", "score": 1.3890912739512353}, {"caption": "\\section{}", "snippet": "\\section{$1}", "meta": "misccorr-cmd", "score": 3.0952612541683835}, {"caption": "\\csname", "snippet": "\\csname", "meta": "misccorr-cmd", "score": 0.008565354665444157}, {"caption": "\\makelabel", "snippet": "\\makelabel", "meta": "misccorr-cmd", "score": 5.739925426740175e-05}, {"caption": "\\makelabel{}", "snippet": "\\makelabel{$1}", "meta": "misccorr-cmd", "score": 5.739925426740175e-05}, {"caption": "\\makelabel[]{}", "snippet": "\\makelabel[$1]{$2}", "meta": "misccorr-cmd", "score": 5.739925426740175e-05}, {"caption": "\\frak{}", "snippet": "\\frak{$1}", "meta": "misccorr-cmd", "score": 0.0017966000518546787}, {"caption": "\\checkmark", "snippet": "\\checkmark", "meta": "misccorr-cmd", "score": 0.025060530944368123}, {"caption": "\\bold", "snippet": "\\bold", "meta": "misccorr-cmd", "score": 0.0014358547624941567}, {"caption": "\\bold{}", "snippet": "\\bold{$1}", "meta": "misccorr-cmd", "score": 0.0014358547624941567}, {"caption": "\\Bbb{}", "snippet": "\\Bbb{$1}", "meta": "misccorr-cmd", "score": 0.0006671850995492977}, {"caption": "\\Bbb", "snippet": "\\Bbb", "meta": "misccorr-cmd", "score": 0.0006671850995492977}], "academicons": [{"caption": "\\aiResearchGateSquare", "snippet": "\\aiResearchGateSquare", "meta": "academicons-cmd", "score": 0.0005747853176838451}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "academicons-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "academicons-cmd", "score": 0.2864294797053033}], "tasks": [{"caption": "\\csname", "snippet": "\\csname", "meta": "tasks-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tasks-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tasks-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tasks-cmd", "score": 0.2864294797053033}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tasks-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tasks-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tasks-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "tasks-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "tasks-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "tasks-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "tasks-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "tasks-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "tasks-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "tasks-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "tasks-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "tasks-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "tasks-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "tasks-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "tasks-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "tasks-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "tasks-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "tasks-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "tasks-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tasks-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "tasks-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "tasks-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "tasks-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "tasks-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tasks-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tasks-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tasks-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tasks-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tasks-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tasks-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tasks-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tasks-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tasks-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tasks-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tasks-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tasks-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tasks-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tasks-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tasks-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tasks-cmd", "score": 0.2864294797053033}], "pstricks-add": [{"caption": "\\green", "snippet": "\\green", "meta": "pstricks-add-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pstricks-add-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pstricks-add-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pstricks-add-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pstricks-add-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pstricks-add-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pstricks-add-cmd", "score": 0.006520475264573554}], "extramarks": [{"caption": "\\leftmark", "snippet": "\\leftmark", "meta": "extramarks-cmd", "score": 0.01094124445235767}, {"caption": "\\extramarks{}{}", "snippet": "\\extramarks{$1}{$2}", "meta": "extramarks-cmd", "score": 0.0003269562507660904}, {"caption": "\\markboth{}{}", "snippet": "\\markboth{$1}{$2}", "meta": "extramarks-cmd", "score": 0.038323601301945065}, {"caption": "\\markboth{}", "snippet": "\\markboth{$1}", "meta": "extramarks-cmd", "score": 0.038323601301945065}, {"caption": "\\markright{}", "snippet": "\\markright{$1}", "meta": "extramarks-cmd", "score": 0.007138622674767024}, {"caption": "\\markright{}{}", "snippet": "\\markright{$1}{$2}", "meta": "extramarks-cmd", "score": 0.007138622674767024}, {"caption": "\\rightmark", "snippet": "\\rightmark", "meta": "extramarks-cmd", "score": 0.008472328846194114}], "calrsfs": [{"caption": "\\mathcal{}", "snippet": "\\mathcal{$1}", "meta": "calrsfs-cmd", "score": 0.35084018920966636}], "newlfont": [{"caption": "\\em", "snippet": "\\em", "meta": "newlfont-cmd", "score": 0.10357353994640862}], "mdwtab": [{"caption": "\\cline{}", "snippet": "\\cline{$1}", "meta": "mdwtab-cmd", "score": 0.07276573550543858}, {"caption": "\\hline", "snippet": "\\hline", "meta": "mdwtab-cmd", "score": 1.3209538327406387}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "mdwtab-cmd", "score": 0.5473606021405326}], "mdwmath": [{"caption": "\\bigg", "snippet": "\\bigg", "meta": "mdwmath-cmd", "score": 0.04318078602869565}], "wallpaper": [{"caption": "\\CenterWallPaper{}{}", "snippet": "\\CenterWallPaper{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.00042983945496357105}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "wallpaper-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "wallpaper-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "wallpaper-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "wallpaper-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "wallpaper-cmd", "score": 0.028955796305270766}, {"caption": "\\empty", "snippet": "\\empty", "meta": "wallpaper-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "wallpaper-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "wallpaper-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "wallpaper-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "wallpaper-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "wallpaper-cmd", "score": 0.008565354665444157}, {"caption": "\\AddToShipoutPictureFG{}", "snippet": "\\AddToShipoutPictureFG{$1}", "meta": "wallpaper-cmd", "score": 0.000325977535138643}, {"caption": "\\AddToShipoutPictureBG{}", "snippet": "\\AddToShipoutPictureBG{$1}", "meta": "wallpaper-cmd", "score": 0.0008957666085644653}, {"caption": "\\AtPageUpperLeft{}", "snippet": "\\AtPageUpperLeft{$1}", "meta": "wallpaper-cmd", "score": 0.0003608141410278152}, {"caption": "\\LenToUnit{}", "snippet": "\\LenToUnit{$1}", "meta": "wallpaper-cmd", "score": 0.0007216282820556304}, {"caption": "\\AddToShipoutPicture{}", "snippet": "\\AddToShipoutPicture{$1}", "meta": "wallpaper-cmd", "score": 0.0017658629469099734}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "wallpaper-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "wallpaper-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "wallpaper-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "wallpaper-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "wallpaper-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "wallpaper-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "wallpaper-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "wallpaper-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "wallpaper-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "wallpaper-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "wallpaper-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "wallpaper-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "wallpaper-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "wallpaper-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "wallpaper-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "wallpaper-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "wallpaper-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "wallpaper-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "wallpaper-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "wallpaper-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "wallpaper-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "wallpaper-cmd", "score": 0.008565354665444157}], "newunicodechar": [{"caption": "\\newunicodechar{}{}", "snippet": "\\newunicodechar{$1}{$2}", "meta": "newunicodechar-cmd", "score": 8.718084183564492e-05}], "thmtools": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "thmtools-cmd", "score": 0.00037306820619479756}, {"caption": "\\listtheoremname", "snippet": "\\listtheoremname", "meta": "thmtools-cmd", "score": 1.9443373798666845e-05}, {"caption": "\\thmtformatoptarg", "snippet": "\\thmtformatoptarg", "meta": "thmtools-cmd", "score": 6.353668036093916e-05}, {"caption": "\\listoftheorems[]", "snippet": "\\listoftheorems[$1]", "meta": "thmtools-cmd", "score": 1.9443373798666845e-05}, {"caption": "\\declaretheoremstyle[]{}", "snippet": "\\declaretheoremstyle[$1]{$2}", "meta": "thmtools-cmd", "score": 0.0001168034231635369}, {"caption": "\\declaretheorem[]{}", "snippet": "\\declaretheorem[$1]{$2}", "meta": "thmtools-cmd", "score": 0.0004904790216915127}, {"caption": "\\theoremstyle{}", "snippet": "\\theoremstyle{$1}", "meta": "thmtools-cmd", "score": 0.02533412165007986}, {"caption": "\\proof{}", "snippet": "\\proof{$1}", "meta": "thmtools-cmd", "score": 0.000701497773639073}, {"caption": "\\proof", "snippet": "\\proof", "meta": "thmtools-cmd", "score": 0.000701497773639073}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "thmtools-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "thmtools-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "thmtools-cmd", "score": 0.215689795055434}, {"caption": "\\endproof", "snippet": "\\endproof", "meta": "thmtools-cmd", "score": 0.0006133100544751855}, {"caption": "\\endproof{}", "snippet": "\\endproof{$1}", "meta": "thmtools-cmd", "score": 0.0006133100544751855}, {"caption": "\\empty", "snippet": "\\empty", "meta": "thmtools-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "thmtools-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "thmtools-cmd", "score": 0.008565354665444157}], "nccmath": [{"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "nccmath-cmd", "score": 1.4341091141105058}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "nccmath-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "nccmath-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "nccmath-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "nccmath-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "nccmath-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "nccmath-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "nccmath-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "nccmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "nccmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "nccmath-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "nccmath-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "nccmath-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "nccmath-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "nccmath-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "nccmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "nccmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "nccmath-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "nccmath-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "nccmath-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "nccmath-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "nccmath-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "nccmath-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "nccmath-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "nccmath-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "nccmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "nccmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "nccmath-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "nccmath-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "nccmath-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "nccmath-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "nccmath-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "nccmath-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "nccmath-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "nccmath-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "nccmath-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "nccmath-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "nccmath-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "nccmath-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "nccmath-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "nccmath-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "nccmath-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "nccmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "nccmath-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "nccmath-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "nccmath-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "nccmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "nccmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "nccmath-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "nccmath-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "nccmath-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "nccmath-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "nccmath-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "nccmath-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "nccmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "nccmath-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "nccmath-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "nccmath-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "nccmath-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "nccmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "nccmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "nccmath-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "nccmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "nccmath-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "nccmath-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "nccmath-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "nccmath-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "nccmath-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "nccmath-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "nccmath-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "nccmath-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "nccmath-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "nccmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "nccmath-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "nccmath-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "nccmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "nccmath-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "nccmath-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "nccmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "nccmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "nccmath-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "nccmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "nccmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "nccmath-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "nccmath-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "nccmath-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "nccmath-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "nccmath-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "nccmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "nccmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "nccmath-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "nccmath-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "nccmath-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "nccmath-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "nccmath-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "nccmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "nccmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "nccmath-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "nccmath-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "nccmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "nccmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "nccmath-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "nccmath-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "nccmath-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "nccmath-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "nccmath-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "nccmath-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "nccmath-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "nccmath-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "nccmath-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "nccmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "nccmath-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "nccmath-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "nccmath-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "nccmath-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "nccmath-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "nccmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "nccmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "nccmath-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "nccmath-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "nccmath-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "nccmath-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "nccmath-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "nccmath-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "nccmath-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "nccmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "nccmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "nccmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "nccmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "nccmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "nccmath-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "nccmath-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "nccmath-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "nccmath-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "nccmath-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "nccmath-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "nccmath-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "nccmath-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "nccmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "nccmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "nccmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "nccmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "nccmath-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "nccmath-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "nccmath-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "nccmath-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "nccmath-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "nccmath-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "nccmath-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "nccmath-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nccmath-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "nccmath-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "nccmath-cmd", "score": 0.0063276692758974925}], "scrtime": [{"caption": "\\newpage", "snippet": "\\newpage", "meta": "scrtime-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "scrtime-cmd", "score": 0.1789117552185788}, {"caption": "\\addtokomafont{}{}", "snippet": "\\addtokomafont{$1}{$2}", "meta": "scrtime-cmd", "score": 0.0008555564394100388}, {"caption": "\\setkomafont{}{}", "snippet": "\\setkomafont{$1}{$2}", "meta": "scrtime-cmd", "score": 0.012985816912639263}, {"caption": "\\KOMAoptions{}", "snippet": "\\KOMAoptions{$1}", "meta": "scrtime-cmd", "score": 0.000396664302361659}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "scrtime-cmd", "score": 0.00037306820619479756}], "luainputenc": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "luainputenc-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "luainputenc-cmd", "score": 0.008565354665444157}], "curve2e": [{"caption": "\\polyline", "snippet": "\\polyline", "meta": "curve2e-cmd", "score": 0.00022468880600368487}, {"caption": "\\put", "snippet": "\\put", "meta": "curve2e-cmd", "score": 0.0406766030275089}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "curve2e-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "curve2e-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "curve2e-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "curve2e-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "curve2e-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "curve2e-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "curve2e-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "curve2e-cmd", "score": 0.2864294797053033}, {"caption": "\\Line", "snippet": "\\Line", "meta": "curve2e-cmd", "score": 0.0006078790177929149}, {"caption": "\\polygon", "snippet": "\\polygon", "meta": "curve2e-cmd", "score": 0.0008987552240147395}, {"caption": "\\line", "snippet": "\\line", "meta": "curve2e-cmd", "score": 0.014519741542622297}, {"caption": "\\polyline", "snippet": "\\polyline", "meta": "curve2e-cmd", "score": 0.00022468880600368487}, {"caption": "\\vector", "snippet": "\\vector", "meta": "curve2e-cmd", "score": 0.002970308722584179}], "couriers": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "couriers-cmd", "score": 0.00037306820619479756}], "caption3": [{"caption": "\\DeclareCaptionJustification{}{}", "snippet": "\\DeclareCaptionJustification{$1}{$2}", "meta": "caption3-cmd", "score": 0.0001872850414971473}, {"caption": "\\DeclareCaptionLabelSeparator{}{}", "snippet": "\\DeclareCaptionLabelSeparator{$1}{$2}", "meta": "caption3-cmd", "score": 0.0003890810058478364}, {"caption": "\\DeclareCaptionFormat{}{}", "snippet": "\\DeclareCaptionFormat{$1}{$2}", "meta": "caption3-cmd", "score": 0.0004717618449370015}, {"caption": "\\DeclareCaptionFont{}{}", "snippet": "\\DeclareCaptionFont{$1}{$2}", "meta": "caption3-cmd", "score": 5.0133404990680195e-05}, {"caption": "\\DeclareCaptionSubType[]{}", "snippet": "\\DeclareCaptionSubType[$1]{$2}", "meta": "caption3-cmd", "score": 0.0001872850414971473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "caption3-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "caption3-cmd", "score": 0.021170869458413965}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "caption3-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "caption3-cmd", "score": 0.02900783226643065}, {"caption": "\\string", "snippet": "\\string", "meta": "caption3-cmd", "score": 0.001042697111754002}, {"caption": "\\DeclareCaptionType{}[][]", "snippet": "\\DeclareCaptionType{$1}[$2][$3]", "meta": "caption3-cmd", "score": 0.00015256647321237863}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "caption3-cmd", "score": 0.00530510025314411}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "caption3-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "caption3-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "caption3-cmd", "score": 0.021473212893597875}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "caption3-cmd", "score": 0.00037306820619479756}], "gauss": [{"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "gauss-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "gauss-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "gauss-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "gauss-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "gauss-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "gauss-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "gauss-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "gauss-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "gauss-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "gauss-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "gauss-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "gauss-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "gauss-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "gauss-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "gauss-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "gauss-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "gauss-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "gauss-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "gauss-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "gauss-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "gauss-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "gauss-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "gauss-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "gauss-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "gauss-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "gauss-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "gauss-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "gauss-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "gauss-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "gauss-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "gauss-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "gauss-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "gauss-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "gauss-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "gauss-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "gauss-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "gauss-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "gauss-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "gauss-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "gauss-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "gauss-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "gauss-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "gauss-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "gauss-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "gauss-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "gauss-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "gauss-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "gauss-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "gauss-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "gauss-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "gauss-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "gauss-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "gauss-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "gauss-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "gauss-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "gauss-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "gauss-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "gauss-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "gauss-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "gauss-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "gauss-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "gauss-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "gauss-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "gauss-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "gauss-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "gauss-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "gauss-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "gauss-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "gauss-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "gauss-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "gauss-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "gauss-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "gauss-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "gauss-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "gauss-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "gauss-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "gauss-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "gauss-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "gauss-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "gauss-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "gauss-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "gauss-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "gauss-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "gauss-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "gauss-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "gauss-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "gauss-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "gauss-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "gauss-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "gauss-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "gauss-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "gauss-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "gauss-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "gauss-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "gauss-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "gauss-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "gauss-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "gauss-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "gauss-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "gauss-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "gauss-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "gauss-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "gauss-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "gauss-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "gauss-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "gauss-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "gauss-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "gauss-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "gauss-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "gauss-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "gauss-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "gauss-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "gauss-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "gauss-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "gauss-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "gauss-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "gauss-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "gauss-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "gauss-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "gauss-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "gauss-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "gauss-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "gauss-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "gauss-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "gauss-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "gauss-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "gauss-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "gauss-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "gauss-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "gauss-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "gauss-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "gauss-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "gauss-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "gauss-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "gauss-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "gauss-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "gauss-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "gauss-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "gauss-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "gauss-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "gauss-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "gauss-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "gauss-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "gauss-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "gauss-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "gauss-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "gauss-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "gauss-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "gauss-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "gauss-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "gauss-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "gauss-cmd", "score": 0.0063276692758974925}], "fancyref": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "fancyref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "fancyref-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "fancyref-cmd", "score": 0.008565354665444157}], "eufrak": [{"caption": "\\mathfrak{}", "snippet": "\\mathfrak{$1}", "meta": "eufrak-cmd", "score": 0.025213895825856578}, {"caption": "\\mathfrak", "snippet": "\\mathfrak", "meta": "eufrak-cmd", "score": 0.025213895825856578}], "fixme": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "fixme-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "fixme-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "fixme-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "fixme-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "fixme-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "fixme-cmd", "score": 0.0018957469739775527}, {"caption": "\\endverbatim", "snippet": "\\endverbatim", "meta": "fixme-cmd", "score": 0.0022216421267780076}, {"caption": "\\verbatim", "snippet": "\\verbatim", "meta": "fixme-cmd", "score": 0.0072203369120285256}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "fixme-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "fixme-cmd", "score": 0.021170869458413965}, {"caption": "\\par", "snippet": "\\par", "meta": "fixme-cmd", "score": 0.413853376001159}, {"caption": "\\verbatiminput{}", "snippet": "\\verbatiminput{$1}", "meta": "fixme-cmd", "score": 0.0024547099784948665}, {"caption": "\\verbatiminput", "snippet": "\\verbatiminput", "meta": "fixme-cmd", "score": 0.0024547099784948665}], "pgf-umlsd": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-umlsd-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgf-umlsd-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgf-umlsd-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgf-umlsd-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgf-umlsd-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgf-umlsd-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgf-umlsd-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgf-umlsd-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgf-umlsd-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-umlsd-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgf-umlsd-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgf-umlsd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgf-umlsd-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgf-umlsd-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "pgf-umlsd-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "pgf-umlsd-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "pgf-umlsd-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "pgf-umlsd-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "pgf-umlsd-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgf-umlsd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgf-umlsd-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgf-umlsd-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgf-umlsd-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgf-umlsd-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgf-umlsd-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgf-umlsd-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgf-umlsd-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgf-umlsd-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgf-umlsd-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-umlsd-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgf-umlsd-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgf-umlsd-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgf-umlsd-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgf-umlsd-cmd", "score": 0.2864294797053033}], "tgadventor": [{"caption": "\\empty", "snippet": "\\empty", "meta": "tgadventor-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgadventor-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgadventor-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgadventor-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgadventor-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgadventor-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgadventor-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgadventor-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tgadventor-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tgadventor-cmd", "score": 0.021170869458413965}], "fancyheadings": [{"caption": "\\lhead{}", "snippet": "\\lhead{$1}", "meta": "fancyheadings-cmd", "score": 0.05268978171228714}, {"caption": "\\chaptermark", "snippet": "\\chaptermark", "meta": "fancyheadings-cmd", "score": 0.005924520024686584}, {"caption": "\\chaptermark{}", "snippet": "\\chaptermark{$1}", "meta": "fancyheadings-cmd", "score": 0.005924520024686584}, {"caption": "\\fancypagestyle{}{}", "snippet": "\\fancypagestyle{$1}{$2}", "meta": "fancyheadings-cmd", "score": 0.009430919590937878}, {"caption": "\\footrule", "snippet": "\\footrule", "meta": "fancyheadings-cmd", "score": 0.0010032754348913366}, {"caption": "\\footrule{}", "snippet": "\\footrule{$1}", "meta": "fancyheadings-cmd", "score": 0.0010032754348913366}, {"caption": "\\fancyfoot[]{}", "snippet": "\\fancyfoot[$1]{$2}", "meta": "fancyheadings-cmd", "score": 0.024973618823189894}, {"caption": "\\fancyfoot{}", "snippet": "\\fancyfoot{$1}", "meta": "fancyheadings-cmd", "score": 0.024973618823189894}, {"caption": "\\fancyfootoffset[]{}", "snippet": "\\fancyfootoffset[$1]{$2}", "meta": "fancyheadings-cmd", "score": 0.0015373246231684555}, {"caption": "\\fancyfootoffset{}", "snippet": "\\fancyfootoffset{$1}", "meta": "fancyheadings-cmd", "score": 0.0015373246231684555}, {"caption": "\\footruleskip", "snippet": "\\footruleskip", "meta": "fancyheadings-cmd", "score": 0.000830117957327721}, {"caption": "\\fancyheadoffset[]{}", "snippet": "\\fancyheadoffset[$1]{$2}", "meta": "fancyheadings-cmd", "score": 0.0016786568695309166}, {"caption": "\\fancyheadoffset{}", "snippet": "\\fancyheadoffset{$1}", "meta": "fancyheadings-cmd", "score": 0.0016786568695309166}, {"caption": "\\iffloatpage{}{}", "snippet": "\\iffloatpage{$1}{$2}", "meta": "fancyheadings-cmd", "score": 6.606286310833368e-05}, {"caption": "\\cfoot{}", "snippet": "\\cfoot{$1}", "meta": "fancyheadings-cmd", "score": 0.013411641301057813}, {"caption": "\\subsectionmark", "snippet": "\\subsectionmark", "meta": "fancyheadings-cmd", "score": 3.1153423008593836e-05}, {"caption": "\\footrulewidth", "snippet": "\\footrulewidth", "meta": "fancyheadings-cmd", "score": 0.011424740897486949}, {"caption": "\\fancyhfoffset[]{}", "snippet": "\\fancyhfoffset[$1]{$2}", "meta": "fancyheadings-cmd", "score": 3.741978601121172e-05}, {"caption": "\\rhead{}", "snippet": "\\rhead{$1}", "meta": "fancyheadings-cmd", "score": 0.022782817416731292}, {"caption": "\\fancyplain{}{}", "snippet": "\\fancyplain{$1}{$2}", "meta": "fancyheadings-cmd", "score": 0.007402339896386138}, {"caption": "\\rfoot{}", "snippet": "\\rfoot{$1}", "meta": "fancyheadings-cmd", "score": 0.013393817825547868}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "fancyheadings-cmd", "score": 0.00530510025314411}, {"caption": "\\plainheadrulewidth", "snippet": "\\plainheadrulewidth", "meta": "fancyheadings-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\baselinestretch", "snippet": "\\baselinestretch", "meta": "fancyheadings-cmd", "score": 0.03225350148161425}, {"caption": "\\lfoot{}", "snippet": "\\lfoot{$1}", "meta": "fancyheadings-cmd", "score": 0.00789399846642229}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "fancyheadings-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "fancyheadings-cmd", "score": 0.006776001543888959}, {"caption": "\\fancyhf{}", "snippet": "\\fancyhf{$1}", "meta": "fancyheadings-cmd", "score": 0.02314618933449356}, {"caption": "\\sectionmark", "snippet": "\\sectionmark", "meta": "fancyheadings-cmd", "score": 0.005008938879210868}, {"caption": "\\fancyhead[]{}", "snippet": "\\fancyhead[$1]{$2}", "meta": "fancyheadings-cmd", "score": 0.039101068064744296}, {"caption": "\\fancyhead{}", "snippet": "\\fancyhead{$1}", "meta": "fancyheadings-cmd", "score": 0.039101068064744296}, {"caption": "\\nouppercase{}", "snippet": "\\nouppercase{$1}", "meta": "fancyheadings-cmd", "score": 0.006416387071584083}, {"caption": "\\nouppercase", "snippet": "\\nouppercase", "meta": "fancyheadings-cmd", "score": 0.006416387071584083}, {"caption": "\\headrule", "snippet": "\\headrule", "meta": "fancyheadings-cmd", "score": 0.0008327432627715623}, {"caption": "\\headrule{}", "snippet": "\\headrule{$1}", "meta": "fancyheadings-cmd", "score": 0.0008327432627715623}, {"caption": "\\chead{}", "snippet": "\\chead{$1}", "meta": "fancyheadings-cmd", "score": 0.00755042164734884}, {"caption": "\\headrulewidth", "snippet": "\\headrulewidth", "meta": "fancyheadings-cmd", "score": 0.02268137935335823}], "tikz-3dplot": [{"caption": "\\tdplotsetmaincoords{}{}", "snippet": "\\tdplotsetmaincoords{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.00021728148272883815}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-3dplot-cmd", "score": 0.008565354665444157}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "tikz-3dplot-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "tikz-3dplot-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "tikz-3dplot-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "tikz-3dplot-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "tikz-3dplot-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-3dplot-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-3dplot-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-3dplot-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-3dplot-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikz-3dplot-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikz-3dplot-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikz-3dplot-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikz-3dplot-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-3dplot-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikz-3dplot-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-3dplot-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikz-3dplot-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-3dplot-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-3dplot-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikz-3dplot-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikz-3dplot-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-3dplot-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-3dplot-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikz-3dplot-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikz-3dplot-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikz-3dplot-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-3dplot-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikz-3dplot-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-3dplot-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikz-3dplot-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikz-3dplot-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikz-3dplot-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikz-3dplot-cmd", "score": 0.2864294797053033}], "ltxtable": [{"caption": "\\let", "snippet": "\\let", "meta": "ltxtable-cmd", "score": 0.03789745970461662}, {"caption": "\\write", "snippet": "\\write", "meta": "ltxtable-cmd", "score": 0.0008038857295393196}, {"caption": "\\tabularxcolumn[]{}", "snippet": "\\tabularxcolumn[$1]{$2}", "meta": "ltxtable-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularxcolumn", "snippet": "\\tabularxcolumn", "meta": "ltxtable-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularx{}{}", "snippet": "\\tabularx{$1}{$2}", "meta": "ltxtable-cmd", "score": 0.0005861357565780464}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ltxtable-cmd", "score": 0.014532521139459619}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "ltxtable-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "ltxtable-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "ltxtable-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ltxtable-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "ltxtable-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ltxtable-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "ltxtable-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "ltxtable-cmd", "score": 0.018615449342361392}, {"caption": "\\endhead", "snippet": "\\endhead", "meta": "ltxtable-cmd", "score": 0.0023853501147448834}, {"caption": "\\endfoot", "snippet": "\\endfoot", "meta": "ltxtable-cmd", "score": 0.00044045261916551967}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "ltxtable-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "ltxtable-cmd", "score": 0.021170869458413965}, {"caption": "\\nopagebreak", "snippet": "\\nopagebreak", "meta": "ltxtable-cmd", "score": 9.952664522415981e-05}, {"caption": "\\endfirsthead", "snippet": "\\endfirsthead", "meta": "ltxtable-cmd", "score": 0.0016148498709822416}, {"caption": "\\endlastfoot", "snippet": "\\endlastfoot", "meta": "ltxtable-cmd", "score": 0.00044045261916551967}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "ltxtable-cmd", "score": 0.3277033727934986}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "ltxtable-cmd", "score": 0.0029238994233674776}, {"caption": "\\pagebreak", "snippet": "\\pagebreak", "meta": "ltxtable-cmd", "score": 0.0313525090421608}], "pict2e": [{"caption": "\\Line", "snippet": "\\Line", "meta": "pict2e-cmd", "score": 0.0006078790177929149}, {"caption": "\\polygon", "snippet": "\\polygon", "meta": "pict2e-cmd", "score": 0.0008987552240147395}, {"caption": "\\line", "snippet": "\\line", "meta": "pict2e-cmd", "score": 0.014519741542622297}, {"caption": "\\polyline", "snippet": "\\polyline", "meta": "pict2e-cmd", "score": 0.00022468880600368487}, {"caption": "\\vector", "snippet": "\\vector", "meta": "pict2e-cmd", "score": 0.002970308722584179}], "ltablex": [{"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "ltablex-cmd", "score": 1.2569477427490174}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "ltablex-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "ltablex-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "ltablex-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ltablex-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "ltablex-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ltablex-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "ltablex-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "ltablex-cmd", "score": 0.018615449342361392}, {"caption": "\\let", "snippet": "\\let", "meta": "ltablex-cmd", "score": 0.03789745970461662}, {"caption": "\\write", "snippet": "\\write", "meta": "ltablex-cmd", "score": 0.0008038857295393196}, {"caption": "\\tabularxcolumn[]{}", "snippet": "\\tabularxcolumn[$1]{$2}", "meta": "ltablex-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularxcolumn", "snippet": "\\tabularxcolumn", "meta": "ltablex-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularx{}{}", "snippet": "\\tabularx{$1}{$2}", "meta": "ltablex-cmd", "score": 0.0005861357565780464}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ltablex-cmd", "score": 0.014532521139459619}, {"caption": "\\endhead", "snippet": "\\endhead", "meta": "ltablex-cmd", "score": 0.0023853501147448834}, {"caption": "\\endfoot", "snippet": "\\endfoot", "meta": "ltablex-cmd", "score": 0.00044045261916551967}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "ltablex-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "ltablex-cmd", "score": 0.021170869458413965}, {"caption": "\\nopagebreak", "snippet": "\\nopagebreak", "meta": "ltablex-cmd", "score": 9.952664522415981e-05}, {"caption": "\\endfirsthead", "snippet": "\\endfirsthead", "meta": "ltablex-cmd", "score": 0.0016148498709822416}, {"caption": "\\endlastfoot", "snippet": "\\endlastfoot", "meta": "ltablex-cmd", "score": 0.00044045261916551967}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "ltablex-cmd", "score": 0.3277033727934986}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "ltablex-cmd", "score": 0.0029238994233674776}, {"caption": "\\pagebreak", "snippet": "\\pagebreak", "meta": "ltablex-cmd", "score": 0.0313525090421608}], "amsopn": [{"caption": "\\sinh", "snippet": "\\sinh", "meta": "amsopn-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "amsopn-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "amsopn-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "amsopn-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "amsopn-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "amsopn-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "amsopn-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "amsopn-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "amsopn-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "amsopn-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "amsopn-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "amsopn-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "amsopn-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "amsopn-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "amsopn-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "amsopn-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "amsopn-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "amsopn-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "amsopn-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "amsopn-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "amsopn-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "amsopn-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "amsopn-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "amsopn-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "amsopn-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "amsopn-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "amsopn-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "amsopn-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "amsopn-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "amsopn-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "amsopn-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "amsopn-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "amsopn-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "amsopn-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "amsopn-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "amsopn-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "amsopn-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "amsopn-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "amsopn-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "amsopn-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "amsopn-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "amsopn-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "amsopn-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "amsopn-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "amsopn-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "amsopn-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "amsopn-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "amsopn-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "amsopn-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "amsopn-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "amsopn-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "amsopn-cmd", "score": 0.0004286136584068833}, {"caption": "\\do", "snippet": "\\do", "meta": "amsopn-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "amsopn-cmd", "score": 0.0063276692758974925}], "topcoman": [{"caption": "\\listing{}", "snippet": "\\listing{$1}", "meta": "topcoman-cmd", "score": 0.00023765162173466673}, {"caption": "\\micro", "snippet": "\\micro", "meta": "topcoman-cmd", "score": 0.011051971930487929}, {"caption": "\\gradi", "snippet": "\\gradi", "meta": "topcoman-cmd", "score": 0.00023765162173466673}, {"caption": "\\unit[]{}", "snippet": "\\unit[$1]{$2}", "meta": "topcoman-cmd", "score": 0.028299796173135428}, {"caption": "\\unit{}", "snippet": "\\unit{$1}", "meta": "topcoman-cmd", "score": 0.028299796173135428}, {"caption": "\\ped{}", "snippet": "\\ped{$1}", "meta": "topcoman-cmd", "score": 0.0007129548652040002}, {"caption": "\\ohm", "snippet": "\\ohm", "meta": "topcoman-cmd", "score": 0.0038146685721293138}, {"caption": "\\gei", "snippet": "\\gei", "meta": "topcoman-cmd", "score": 0.00023765162173466673}], "topfront": [{"caption": "\\corsodilaurea{}", "snippet": "\\corsodilaurea{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\NomeQuartoTomo{}", "snippet": "\\NomeQuartoTomo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\ciclodidottorato{}", "snippet": "\\ciclodidottorato{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\CorsoDiLaureaIn{}", "snippet": "\\CorsoDiLaureaIn{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\ateneo{}", "snippet": "\\ateneo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\retrofrontespizio{}", "snippet": "\\retrofrontespizio{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\InName{}", "snippet": "\\InName{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\secondocandidato{}", "snippet": "\\secondocandidato{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\NomeMonografia{}", "snippet": "\\NomeMonografia{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\NomeTutoreAziendale{}", "snippet": "\\NomeTutoreAziendale{$1}", "meta": "topfront-cmd", "score": 0.00047530324346933345}, {"caption": "\\TutorName{}", "snippet": "\\TutorName{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\NomeDissertazione{}", "snippet": "\\NomeDissertazione{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\sedutadilaurea{}", "snippet": "\\sedutadilaurea{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\logosede{}", "snippet": "\\logosede{$1}", "meta": "topfront-cmd", "score": 0.00047530324346933345}, {"caption": "\\TesiDiLaurea{}", "snippet": "\\TesiDiLaurea{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\NomeTerzoTomo{}", "snippet": "\\NomeTerzoTomo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\AdvisorName{}", "snippet": "\\AdvisorName{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\facolta[]{}", "snippet": "\\facolta[$1]{$2}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\CycleName{}", "snippet": "\\CycleName{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\NomePrimoTomo{}", "snippet": "\\NomePrimoTomo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\candidato{}", "snippet": "\\candidato{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\NomeSecondoTomo{}", "snippet": "\\NomeSecondoTomo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\titolo{}", "snippet": "\\titolo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\CandidateName{}", "snippet": "\\CandidateName{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\secondorelatore{}", "snippet": "\\secondorelatore{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\FacoltaDi{}", "snippet": "\\FacoltaDi{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\nomeateneo{}", "snippet": "\\nomeateneo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\DottoratoIn{}", "snippet": "\\DottoratoIn{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\sottotitolo{}", "snippet": "\\sottotitolo{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\relatore{}", "snippet": "\\relatore{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}, {"caption": "\\tutoreaziendale{}", "snippet": "\\tutoreaziendale{$1}", "meta": "topfront-cmd", "score": 0.00023765162173466673}], "mathspec": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "mathspec-cmd", "score": 0.00021116765384691477}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "mathspec-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "mathspec-cmd", "score": 0.2864294797053033}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "mathspec-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "mathspec-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "mathspec-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "mathspec-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "mathspec-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "mathspec-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "mathspec-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "mathspec-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "mathspec-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "mathspec-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "mathspec-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "mathspec-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "mathspec-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "mathspec-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "mathspec-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "mathspec-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mathspec-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "mathspec-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "mathspec-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "mathspec-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "mathspec-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mathspec-cmd", "score": 0.008565354665444157}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "mathspec-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "mathspec-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "mathspec-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mathspec-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "mathspec-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "mathspec-cmd", "score": 0.0063276692758974925}], "overpic": [{"caption": "\\csname", "snippet": "\\csname", "meta": "overpic-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "overpic-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "overpic-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "overpic-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "overpic-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "overpic-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "overpic-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "overpic-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "overpic-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "overpic-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "overpic-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "overpic-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "overpic-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "overpic-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "overpic-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "overpic-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "overpic-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "overpic-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "overpic-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "overpic-cmd", "score": 0.004719094298848707}], "tkz-euclide": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-euclide-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-euclide-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-euclide-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-euclide-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-euclide-cmd", "score": 0.004719094298848707}, {"caption": "\\reserveinserts{}", "snippet": "\\reserveinserts{$1}", "meta": "tkz-euclide-cmd", "score": 0.0018653410309739879}, {"caption": "\\newtoks", "snippet": "\\newtoks", "meta": "tkz-euclide-cmd", "score": 0.00031058155311734754}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-euclide-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-euclide-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tkz-euclide-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tkz-euclide-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tkz-euclide-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tkz-euclide-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-euclide-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tkz-euclide-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-euclide-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tkz-euclide-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-euclide-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-euclide-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tkz-euclide-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tkz-euclide-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-euclide-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-euclide-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tkz-euclide-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tkz-euclide-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tkz-euclide-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-euclide-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tkz-euclide-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-euclide-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tkz-euclide-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tkz-euclide-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tkz-euclide-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tkz-euclide-cmd", "score": 0.2864294797053033}], "morewrites": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "morewrites-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "morewrites-cmd", "score": 0.2864294797053033}], "pgflibraryshapes": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgflibraryshapes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgflibraryshapes-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgflibraryshapes-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgflibraryshapes-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgflibraryshapes-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgflibraryshapes-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgflibraryshapes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgflibraryshapes-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgflibraryshapes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgflibraryshapes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgflibraryshapes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgflibraryshapes-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgflibraryshapes-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgflibraryshapes-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgflibraryshapes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgflibraryshapes-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgflibraryshapes-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgflibraryshapes-cmd", "score": 0.2864294797053033}], "pdfcolparallel": [{"caption": "\\empty", "snippet": "\\empty", "meta": "pdfcolparallel-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "pdfcolparallel-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "pdfcolparallel-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pdfcolparallel-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pdfcolparallel-cmd", "score": 0.00037306820619479756}, {"caption": "\\ParallelRText{}", "snippet": "\\ParallelRText{$1}", "meta": "pdfcolparallel-cmd", "score": 0.0005986518360651812}, {"caption": "\\ParallelLText{}", "snippet": "\\ParallelLText{$1}", "meta": "pdfcolparallel-cmd", "score": 0.0005986518360651812}, {"caption": "\\ParallelPar", "snippet": "\\ParallelPar", "meta": "pdfcolparallel-cmd", "score": 0.0005986518360651812}], "aeguill": [{"caption": "\\guillemotleft", "snippet": "\\guillemotleft", "meta": "aeguill-cmd", "score": 9.764370963946686e-05}, {"caption": "\\guillemotright", "snippet": "\\guillemotright", "meta": "aeguill-cmd", "score": 9.764370963946686e-05}, {"caption": "\\sfdefault", "snippet": "\\sfdefault", "meta": "aeguill-cmd", "score": 0.008427383388519996}, {"caption": "\\sfdefault{}", "snippet": "\\sfdefault{$1}", "meta": "aeguill-cmd", "score": 0.008427383388519996}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "aeguill-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "aeguill-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "aeguill-cmd", "score": 0.021170869458413965}], "changes": [{"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "changes-cmd", "score": 0.04598628699063736}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "changes-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "changes-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "changes-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "changes-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "changes-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "changes-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "changes-cmd", "score": 0.028955796305270766}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "changes-cmd", "score": 0.01590723355124104}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "changes-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "changes-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "changes-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "changes-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "changes-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "changes-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "changes-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "changes-cmd", "score": 0.0018957469739775527}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "changes-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "changes-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "changes-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "changes-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "changes-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "changes-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "changes-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "changes-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "changes-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "changes-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "changes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "changes-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "changes-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "changes-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "changes-cmd", "score": 0.2864294797053033}], "droidmono": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "droidmono-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "droidmono-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "droidmono-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "droidmono-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "droidmono-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "droidmono-cmd", "score": 0.0018957469739775527}, {"caption": "\\scshape", "snippet": "\\scshape", "meta": "droidmono-cmd", "score": 0.05364108855914402}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "droidmono-cmd", "score": 0.00037306820619479756}], "tgheros": [{"caption": "\\sfdefault", "snippet": "\\sfdefault", "meta": "tgheros-cmd", "score": 0.008427383388519996}, {"caption": "\\sfdefault{}", "snippet": "\\sfdefault{$1}", "meta": "tgheros-cmd", "score": 0.008427383388519996}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgheros-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgheros-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgheros-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgheros-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgheros-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgheros-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgheros-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgheros-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tgheros-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tgheros-cmd", "score": 0.021170869458413965}], "har2nat": [{"caption": "\\citeasnoun{}", "snippet": "\\citeasnoun{$1}", "meta": "har2nat-cmd", "score": 0.010452591644582749}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "har2nat-cmd", "score": 2.341195220791228}, {"caption": "\\citealt{}", "snippet": "\\citealt{$1}", "meta": "har2nat-cmd", "score": 0.007302105441724955}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "har2nat-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "har2nat-cmd", "score": 0.021170869458413965}, {"caption": "\\textsuperscript{}", "snippet": "\\textsuperscript{$1}", "meta": "har2nat-cmd", "score": 0.05216393882408519}, {"caption": "\\nocite{}", "snippet": "\\nocite{$1}", "meta": "har2nat-cmd", "score": 0.04990693820960752}, {"caption": "\\bibname", "snippet": "\\bibname", "meta": "har2nat-cmd", "score": 0.007599529252128519}, {"caption": "\\bibname{}", "snippet": "\\bibname{$1}", "meta": "har2nat-cmd", "score": 0.007599529252128519}, {"caption": "\\bibpunct", "snippet": "\\bibpunct", "meta": "har2nat-cmd", "score": 0.001148574749873469}, {"caption": "\\bibpunct{}{}{}{}{}{}", "snippet": "\\bibpunct{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "har2nat-cmd", "score": 0.001148574749873469}, {"caption": "\\bibpunct[]{}{}{}{}{}{}", "snippet": "\\bibpunct[$1]{$2}{$3}{$4}{$5}{$6}{$7}", "meta": "har2nat-cmd", "score": 0.001148574749873469}, {"caption": "\\citepalias{}", "snippet": "\\citepalias{$1}", "meta": "har2nat-cmd", "score": 0.00032712684909035603}, {"caption": "\\citepalias[][]{}", "snippet": "\\citepalias[$1][$2]{$3}", "meta": "har2nat-cmd", "score": 0.00032712684909035603}, {"caption": "\\makeindex", "snippet": "\\makeindex", "meta": "har2nat-cmd", "score": 0.010304996748556729}, {"caption": "\\citep{}", "snippet": "\\citep{$1}", "meta": "har2nat-cmd", "score": 0.2941882834697057}, {"caption": "\\bibsection", "snippet": "\\bibsection", "meta": "har2nat-cmd", "score": 0.00038872734530908233}, {"caption": "\\bibsection{}", "snippet": "\\bibsection{$1}", "meta": "har2nat-cmd", "score": 0.00038872734530908233}, {"caption": "\\refname", "snippet": "\\refname", "meta": "har2nat-cmd", "score": 0.006490238196722249}, {"caption": "\\refname{}", "snippet": "\\refname{$1}", "meta": "har2nat-cmd", "score": 0.006490238196722249}, {"caption": "\\citealp{}", "snippet": "\\citealp{$1}", "meta": "har2nat-cmd", "score": 0.005275912376595364}, {"caption": "\\citealp[]{}", "snippet": "\\citealp[$1]{$2}", "meta": "har2nat-cmd", "score": 0.005275912376595364}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "har2nat-cmd", "score": 2.341195220791228}, {"caption": "\\citetalias{}", "snippet": "\\citetalias{$1}", "meta": "har2nat-cmd", "score": 0.001419571355756266}, {"caption": "\\bibitem{}", "snippet": "\\bibitem{$1}", "meta": "har2nat-cmd", "score": 0.3689547570562042}, {"caption": "\\bibitem[]{}", "snippet": "\\bibitem[$1]{$2}", "meta": "har2nat-cmd", "score": 0.3689547570562042}, {"caption": "\\citet{}", "snippet": "\\citet{$1}", "meta": "har2nat-cmd", "score": 0.09046048561361801}, {"caption": "\\defcitealias{}{}", "snippet": "\\defcitealias{$1}{$2}", "meta": "har2nat-cmd", "score": 0.00042021825647418025}, {"caption": "\\aftergroup", "snippet": "\\aftergroup", "meta": "har2nat-cmd", "score": 0.002020423627422133}, {"caption": "\\setcitestyle{}", "snippet": "\\setcitestyle{$1}", "meta": "har2nat-cmd", "score": 0.0015840652870152204}, {"caption": "\\citeyearpar{}", "snippet": "\\citeyearpar{$1}", "meta": "har2nat-cmd", "score": 0.001877888310324327}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "har2nat-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "har2nat-cmd", "score": 0.006776001543888959}, {"caption": "\\newblock", "snippet": "\\newblock", "meta": "har2nat-cmd", "score": 0.03684301726876973}, {"caption": "\\newblock{}", "snippet": "\\newblock{$1}", "meta": "har2nat-cmd", "score": 0.03684301726876973}, {"caption": "\\bibnumfmt", "snippet": "\\bibnumfmt", "meta": "har2nat-cmd", "score": 0.000353353600267394}, {"caption": "\\citeyear{}", "snippet": "\\citeyear{$1}", "meta": "har2nat-cmd", "score": 0.01091041305836494}, {"caption": "\\citeauthor{}", "snippet": "\\citeauthor{$1}", "meta": "har2nat-cmd", "score": 0.01359248786373484}, {"caption": "\\let", "snippet": "\\let", "meta": "har2nat-cmd", "score": 0.03789745970461662}], "matlab-prettifier": [{"caption": "\\mlttfamily", "snippet": "\\mlttfamily", "meta": "matlab-prettifier-cmd", "score": 0.000856282742498241}, {"caption": "\\vskip", "snippet": "\\vskip", "meta": "matlab-prettifier-cmd", "score": 0.05143052892347224}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "matlab-prettifier-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "matlab-prettifier-cmd", "score": 0.021170869458413965}, {"caption": "\\do", "snippet": "\\do", "meta": "matlab-prettifier-cmd", "score": 0.009278344180101056}, {"caption": "\\thelstlisting", "snippet": "\\thelstlisting", "meta": "matlab-prettifier-cmd", "score": 0.00012774128088872144}, {"caption": "\\lstinputlisting[]{}", "snippet": "\\lstinputlisting[$1]{$2}", "meta": "matlab-prettifier-cmd", "score": 0.011660477607086044}, {"caption": "\\lstinputlisting{}", "snippet": "\\lstinputlisting{$1}", "meta": "matlab-prettifier-cmd", "score": 0.011660477607086044}, {"caption": "\\space", "snippet": "\\space", "meta": "matlab-prettifier-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "matlab-prettifier-cmd", "score": 0.008565354665444157}, {"caption": "\\lstinline", "snippet": "\\lstinline", "meta": "matlab-prettifier-cmd", "score": 0.005972262850694285}, {"caption": "\\lstinline{}", "snippet": "\\lstinline{$1}", "meta": "matlab-prettifier-cmd", "score": 0.005972262850694285}, {"caption": "\\lstlistoflistings", "snippet": "\\lstlistoflistings", "meta": "matlab-prettifier-cmd", "score": 0.005279080363360602}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "matlab-prettifier-cmd", "score": 0.00037306820619479756}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "matlab-prettifier-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "matlab-prettifier-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "matlab-prettifier-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "matlab-prettifier-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "matlab-prettifier-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "matlab-prettifier-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "matlab-prettifier-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "matlab-prettifier-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "matlab-prettifier-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "matlab-prettifier-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "matlab-prettifier-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "matlab-prettifier-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "matlab-prettifier-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "matlab-prettifier-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "matlab-prettifier-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "matlab-prettifier-cmd", "score": 0.2864294797053033}], "datetime2": [{"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "datetime2-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "datetime2-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "datetime2-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "datetime2-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "datetime2-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "datetime2-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "datetime2-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "datetime2-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "datetime2-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "datetime2-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "datetime2-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "datetime2-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "datetime2-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "datetime2-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "datetime2-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "datetime2-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "datetime2-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "datetime2-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "datetime2-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "datetime2-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "datetime2-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "datetime2-cmd", "score": 0.008565354665444157}], "lapdf": [{"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "lapdf-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "lapdf-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "lapdf-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "lapdf-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "lapdf-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "lapdf-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "lapdf-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "lapdf-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "lapdf-cmd", "score": 0.028955796305270766}], "nccbbb": [{"caption": "\\bbbe", "snippet": "\\bbbe", "meta": "nccbbb-cmd", "score": 0.0013332214754983353}, {"caption": "\\bbbe[]", "snippet": "\\bbbe[$1]", "meta": "nccbbb-cmd", "score": 0.0013332214754983353}, {"caption": "\\bbbr", "snippet": "\\bbbr", "meta": "nccbbb-cmd", "score": 0.0015739010274051707}], "tgbonum": [{"caption": "\\empty", "snippet": "\\empty", "meta": "tgbonum-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgbonum-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgbonum-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgbonum-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgbonum-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgbonum-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgbonum-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgbonum-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tgbonum-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tgbonum-cmd", "score": 0.021170869458413965}], "thm-restate": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "thm-restate-cmd", "score": 0.00037306820619479756}, {"caption": "\\listtheoremname", "snippet": "\\listtheoremname", "meta": "thm-restate-cmd", "score": 1.9443373798666845e-05}, {"caption": "\\thmtformatoptarg", "snippet": "\\thmtformatoptarg", "meta": "thm-restate-cmd", "score": 6.353668036093916e-05}, {"caption": "\\listoftheorems[]", "snippet": "\\listoftheorems[$1]", "meta": "thm-restate-cmd", "score": 1.9443373798666845e-05}, {"caption": "\\declaretheoremstyle[]{}", "snippet": "\\declaretheoremstyle[$1]{$2}", "meta": "thm-restate-cmd", "score": 0.0001168034231635369}, {"caption": "\\declaretheorem[]{}", "snippet": "\\declaretheorem[$1]{$2}", "meta": "thm-restate-cmd", "score": 0.0004904790216915127}, {"caption": "\\theoremstyle{}", "snippet": "\\theoremstyle{$1}", "meta": "thm-restate-cmd", "score": 0.02533412165007986}, {"caption": "\\empty", "snippet": "\\empty", "meta": "thm-restate-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "thm-restate-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "thm-restate-cmd", "score": 0.008565354665444157}, {"caption": "\\proof{}", "snippet": "\\proof{$1}", "meta": "thm-restate-cmd", "score": 0.000701497773639073}, {"caption": "\\proof", "snippet": "\\proof", "meta": "thm-restate-cmd", "score": 0.000701497773639073}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "thm-restate-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "thm-restate-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "thm-restate-cmd", "score": 0.215689795055434}, {"caption": "\\endproof", "snippet": "\\endproof", "meta": "thm-restate-cmd", "score": 0.0006133100544751855}, {"caption": "\\endproof{}", "snippet": "\\endproof{$1}", "meta": "thm-restate-cmd", "score": 0.0006133100544751855}], "biblatex-chicago": [{"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "biblatex-chicago-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "biblatex-chicago-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "biblatex-chicago-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "biblatex-chicago-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "biblatex-chicago-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "biblatex-chicago-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "biblatex-chicago-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "biblatex-chicago-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "biblatex-chicago-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "biblatex-chicago-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "biblatex-chicago-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "biblatex-chicago-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "biblatex-chicago-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "biblatex-chicago-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "biblatex-chicago-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "biblatex-chicago-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "biblatex-chicago-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "biblatex-chicago-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "biblatex-chicago-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "biblatex-chicago-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "biblatex-chicago-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "biblatex-chicago-cmd", "score": 0.008565354665444157}], "pseudocode": [{"caption": "\\shadowbox{}", "snippet": "\\shadowbox{$1}", "meta": "pseudocode-cmd", "score": 0.00107667147399019}, {"caption": "\\doublebox", "snippet": "\\doublebox", "meta": "pseudocode-cmd", "score": 0.00015142240898356106}, {"caption": "\\VerbatimEnvironment", "snippet": "\\VerbatimEnvironment", "meta": "pseudocode-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\thisfancypage{}{}", "snippet": "\\thisfancypage{$1}{$2}", "meta": "pseudocode-cmd", "score": 0.00015142240898356106}, {"caption": "\\TheSbox", "snippet": "\\TheSbox", "meta": "pseudocode-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "pseudocode-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "pseudocode-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "pseudocode-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "pseudocode-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "pseudocode-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "pseudocode-cmd", "score": 0.0018957469739775527}], "imakeidx": [{"caption": "\\makeindex", "snippet": "\\makeindex", "meta": "imakeidx-cmd", "score": 0.010304996748556729}, {"caption": "\\printindex", "snippet": "\\printindex", "meta": "imakeidx-cmd", "score": 0.004417016910870522}, {"caption": "\\index{}", "snippet": "\\index{$1}", "meta": "imakeidx-cmd", "score": 0.013774721817648336}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "imakeidx-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "imakeidx-cmd", "score": 0.008565354665444157}], "uri": [{"caption": "\\empty", "snippet": "\\empty", "meta": "uri-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "uri-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "uri-cmd", "score": 0.008565354665444157}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "uri-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "uri-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "uri-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "uri-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "uri-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "uri-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "uri-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "uri-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "uri-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "uri-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "uri-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\empty", "snippet": "\\empty", "meta": "uri-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "uri-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "uri-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "uri-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "uri-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "uri-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "uri-cmd", "score": 0.021170869458413965}], "tocvsec2": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "tocvsec2-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "tocvsec2-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "tocvsec2-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "tocvsec2-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "tocvsec2-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "tocvsec2-cmd", "score": 0.0018957469739775527}], "graphbox": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "graphbox-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "graphbox-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "graphbox-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "graphbox-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "graphbox-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "graphbox-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "graphbox-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "graphbox-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "graphbox-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "graphbox-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "graphbox-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "graphbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "graphbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "graphbox-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "graphbox-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "graphbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "graphbox-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "graphbox-cmd", "score": 0.004719094298848707}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "graphbox-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "graphbox-cmd", "score": 0.008565354665444157}], "limap": [{"caption": "\\MapContinuing{}", "snippet": "\\MapContinuing{$1}", "meta": "limap-cmd", "score": 7.216282820556303e-05}, {"caption": "\\MapTextFraction{}", "snippet": "\\MapTextFraction{$1}", "meta": "limap-cmd", "score": 7.216282820556303e-05}, {"caption": "\\MapBlockLabelFont{}", "snippet": "\\MapBlockLabelFont{$1}", "meta": "limap-cmd", "score": 7.216282820556303e-05}, {"caption": "\\Block{}", "snippet": "\\Block{$1}", "meta": "limap-cmd", "score": 0.011618215341095648}, {"caption": "\\MapRuleWidth{}", "snippet": "\\MapRuleWidth{$1}", "meta": "limap-cmd", "score": 7.216282820556303e-05}, {"caption": "\\MapTitleFraction{}", "snippet": "\\MapTitleFraction{$1}", "meta": "limap-cmd", "score": 7.216282820556303e-05}, {"caption": "\\MapContinued{}", "snippet": "\\MapContinued{$1}", "meta": "limap-cmd", "score": 7.216282820556303e-05}, {"caption": "\\WideBlock{}", "snippet": "\\WideBlock{$1}", "meta": "limap-cmd", "score": 0.002453536158989143}, {"caption": "\\MapParskip{}", "snippet": "\\MapParskip{$1}", "meta": "limap-cmd", "score": 7.216282820556303e-05}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "limap-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "limap-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "limap-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "limap-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "limap-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "limap-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "limap-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "limap-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "limap-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "limap-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "limap-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "limap-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "limap-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "limap-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "limap-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "limap-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "limap-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "limap-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "limap-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "limap-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "limap-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "limap-cmd", "score": 0.008565354665444157}, {"caption": "\\specialrule{}{}{}", "snippet": "\\specialrule{$1}{$2}{$3}", "meta": "limap-cmd", "score": 0.004974385202605165}, {"caption": "\\cmidrule", "snippet": "\\cmidrule", "meta": "limap-cmd", "score": 0.01894952272365088}, {"caption": "\\cmidrule{}", "snippet": "\\cmidrule{$1}", "meta": "limap-cmd", "score": 0.01894952272365088}, {"caption": "\\bottomrule", "snippet": "\\bottomrule", "meta": "limap-cmd", "score": 0.04533364657852219}, {"caption": "\\midrule", "snippet": "\\midrule", "meta": "limap-cmd", "score": 0.07098077735912875}, {"caption": "\\addlinespace", "snippet": "\\addlinespace", "meta": "limap-cmd", "score": 0.005865460617491447}, {"caption": "\\addlinespace[]", "snippet": "\\addlinespace[$1]", "meta": "limap-cmd", "score": 0.005865460617491447}, {"caption": "\\toprule", "snippet": "\\toprule", "meta": "limap-cmd", "score": 0.059857788139528495}, {"caption": "\\endhead", "snippet": "\\endhead", "meta": "limap-cmd", "score": 0.0023853501147448834}, {"caption": "\\endfoot", "snippet": "\\endfoot", "meta": "limap-cmd", "score": 0.00044045261916551967}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "limap-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "limap-cmd", "score": 0.021170869458413965}, {"caption": "\\nopagebreak", "snippet": "\\nopagebreak", "meta": "limap-cmd", "score": 9.952664522415981e-05}, {"caption": "\\endfirsthead", "snippet": "\\endfirsthead", "meta": "limap-cmd", "score": 0.0016148498709822416}, {"caption": "\\endlastfoot", "snippet": "\\endlastfoot", "meta": "limap-cmd", "score": 0.00044045261916551967}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "limap-cmd", "score": 0.3277033727934986}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "limap-cmd", "score": 0.0029238994233674776}, {"caption": "\\pagebreak", "snippet": "\\pagebreak", "meta": "limap-cmd", "score": 0.0313525090421608}], "tikzscale": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikzscale-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikzscale-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikzscale-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikzscale-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikzscale-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikzscale-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikzscale-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikzscale-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzscale-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikzscale-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzscale-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikzscale-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikzscale-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikzscale-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikzscale-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikzscale-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikzscale-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikzscale-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikzscale-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzscale-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzscale-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "tikzscale-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "tikzscale-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "tikzscale-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "tikzscale-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "tikzscale-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "tikzscale-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "tikzscale-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "tikzscale-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "tikzscale-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "tikzscale-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "tikzscale-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "tikzscale-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "tikzscale-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "tikzscale-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "tikzscale-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "tikzscale-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikzscale-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "tikzscale-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "tikzscale-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "tikzscale-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "tikzscale-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikzscale-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikzscale-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikzscale-cmd", "score": 0.2864294797053033}], "savesym": [{"caption": "\\savesymbol{}", "snippet": "\\savesymbol{$1}", "meta": "savesym-cmd", "score": 6.662041157021826e-05}], "subscript": [{"caption": "\\textsubscript{}", "snippet": "\\textsubscript{$1}", "meta": "subscript-cmd", "score": 0.058405875394131175}], "letterspace": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "letterspace-cmd", "score": 0.00037306820619479756}], "mathastext": [{"caption": "\\Huge", "snippet": "\\Huge", "meta": "mathastext-cmd", "score": 0.04725806985998919}, {"caption": "\\sfdefault", "snippet": "\\sfdefault", "meta": "mathastext-cmd", "score": 0.008427383388519996}, {"caption": "\\sfdefault{}", "snippet": "\\sfdefault{$1}", "meta": "mathastext-cmd", "score": 0.008427383388519996}, {"caption": "\\implies", "snippet": "\\implies", "meta": "mathastext-cmd", "score": 0.021828316911576096}, {"caption": "\\mathrm{}", "snippet": "\\mathrm{$1}", "meta": "mathastext-cmd", "score": 0.19117752976172653}], "movie15": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "movie15-cmd", "score": 0.00037306820619479756}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "movie15-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "movie15-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "movie15-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "movie15-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "movie15-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "movie15-cmd", "score": 0.0018957469739775527}], "refstyle": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "refstyle-cmd", "score": 0.00037306820619479756}], "pst-3d": [{"caption": "\\green", "snippet": "\\green", "meta": "pst-3d-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pst-3d-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pst-3d-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pst-3d-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pst-3d-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pst-3d-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pst-3d-cmd", "score": 0.006520475264573554}], "rotfloat": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.00037306820619479756}, {"caption": "\\listof{}{}", "snippet": "\\listof{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.0009837365348002915}, {"caption": "\\floatplacement{}{}", "snippet": "\\floatplacement{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.0005815474978918903}, {"caption": "\\restylefloat{}", "snippet": "\\restylefloat{$1}", "meta": "rotfloat-cmd", "score": 0.0008866338267686714}, {"caption": "\\floatstyle{}", "snippet": "\\floatstyle{$1}", "meta": "rotfloat-cmd", "score": 0.0015470917047414941}, {"caption": "\\floatname{}{}", "snippet": "\\floatname{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.0011934321931750752}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rotfloat-cmd", "score": 0.008565354665444157}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "rotfloat-cmd", "score": 1.2569477427490174}, {"caption": "\\newfloat{}{}{}", "snippet": "\\newfloat{$1}{$2}{$3}", "meta": "rotfloat-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat", "snippet": "\\newfloat", "meta": "rotfloat-cmd", "score": 0.0012745874472536625}, {"caption": "\\newfloat{}", "snippet": "\\newfloat{$1}", "meta": "rotfloat-cmd", "score": 0.0012745874472536625}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rotfloat-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "rotfloat-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "rotfloat-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "rotfloat-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "rotfloat-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "rotfloat-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "rotfloat-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "rotfloat-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "rotfloat-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rotfloat-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "rotfloat-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "rotfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "rotfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "rotfloat-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "rotfloat-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "rotfloat-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "rotfloat-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "rotfloat-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "rotfloat-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "rotfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "rotfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "rotfloat-cmd", "score": 0.004719094298848707}], "progressbar": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "progressbar-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "progressbar-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "progressbar-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "progressbar-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "progressbar-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "progressbar-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "progressbar-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "progressbar-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "progressbar-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "progressbar-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "progressbar-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "progressbar-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "progressbar-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "progressbar-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "progressbar-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "progressbar-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "progressbar-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "progressbar-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "progressbar-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "progressbar-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "progressbar-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "progressbar-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "progressbar-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "progressbar-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "progressbar-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "progressbar-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "progressbar-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "progressbar-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "progressbar-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "progressbar-cmd", "score": 0.2864294797053033}, {"caption": "\\empty", "snippet": "\\empty", "meta": "progressbar-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "progressbar-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "progressbar-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "progressbar-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "progressbar-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "progressbar-cmd", "score": 0.008565354665444157}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "progressbar-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "progressbar-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "progressbar-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "progressbar-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "progressbar-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "progressbar-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "progressbar-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "progressbar-cmd", "score": 0.028955796305270766}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "progressbar-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "progressbar-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "progressbar-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "progressbar-cmd", "score": 0.008565354665444157}], "pagecolor": [{"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pagecolor-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pagecolor-cmd", "score": 0.0008147200475678891}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pagecolor-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pagecolor-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pagecolor-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pagecolor-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pagecolor-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pagecolor-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pagecolor-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pagecolor-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pagecolor-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pagecolor-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pagecolor-cmd", "score": 0.021170869458413965}], "gb4e": [{"caption": "\\ex", "snippet": "\\ex", "meta": "gb4e-cmd", "score": 0.00916111174873264}], "ESIEEcv": [{"caption": "\\let", "snippet": "\\let", "meta": "ESIEEcv-cmd", "score": 0.03789745970461662}, {"caption": "\\write", "snippet": "\\write", "meta": "ESIEEcv-cmd", "score": 0.0008038857295393196}, {"caption": "\\tabularxcolumn[]{}", "snippet": "\\tabularxcolumn[$1]{$2}", "meta": "ESIEEcv-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularxcolumn", "snippet": "\\tabularxcolumn", "meta": "ESIEEcv-cmd", "score": 0.00048507499766588637}, {"caption": "\\tabularx{}{}", "snippet": "\\tabularx{$1}{$2}", "meta": "ESIEEcv-cmd", "score": 0.0005861357565780464}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ESIEEcv-cmd", "score": 0.014532521139459619}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "ESIEEcv-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "ESIEEcv-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "ESIEEcv-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "ESIEEcv-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "ESIEEcv-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ESIEEcv-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "ESIEEcv-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "ESIEEcv-cmd", "score": 0.018615449342361392}], "ftnright": [{"caption": "\\footnotesize", "snippet": "\\footnotesize", "meta": "ftnright-cmd", "score": 0.2038592081252624}, {"caption": "\\footnotesize{}", "snippet": "\\footnotesize{$1}", "meta": "ftnright-cmd", "score": 0.2038592081252624}], "chemformula": [{"caption": "\\ch{}", "snippet": "\\ch{$1}", "meta": "chemformula-cmd", "score": 0.0013276105116845872}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "chemformula-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "chemformula-cmd", "score": 0.1789117552185788}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "chemformula-cmd", "score": 0.00037306820619479756}, {"caption": "\\nicefrac{}{}", "snippet": "\\nicefrac{$1}{$2}", "meta": "chemformula-cmd", "score": 0.0018011350423659288}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "chemformula-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "chemformula-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "chemformula-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "chemformula-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "chemformula-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "chemformula-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "chemformula-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "chemformula-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "chemformula-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "chemformula-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "chemformula-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "chemformula-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "chemformula-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "chemformula-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "chemformula-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "chemformula-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "chemformula-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "chemformula-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "chemformula-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "chemformula-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "chemformula-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "chemformula-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "chemformula-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "chemformula-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "chemformula-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "chemformula-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "chemformula-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "chemformula-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "chemformula-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "chemformula-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "chemformula-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "chemformula-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "chemformula-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "chemformula-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "chemformula-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "chemformula-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "chemformula-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "chemformula-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "chemformula-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "chemformula-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "chemformula-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "chemformula-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "chemformula-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "chemformula-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "chemformula-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "chemformula-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "chemformula-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "chemformula-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "chemformula-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "chemformula-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "chemformula-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "chemformula-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "chemformula-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "chemformula-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "chemformula-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemformula-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "chemformula-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "chemformula-cmd", "score": 0.2864294797053033}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chemformula-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chemformula-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "chemformula-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "chemformula-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "chemformula-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "chemformula-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "chemformula-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chemformula-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "chemformula-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemformula-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "chemformula-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chemformula-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chemformula-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chemformula-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "chemformula-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "chemformula-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chemformula-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chemformula-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "chemformula-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "chemformula-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "chemformula-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "chemformula-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "chemformula-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chemformula-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "chemformula-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "chemformula-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemformula-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "chemformula-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "chemformula-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "chemformula-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "chemformula-cmd", "score": 0.2864294797053033}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "chemformula-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "chemformula-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "chemformula-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "chemformula-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "chemformula-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "chemformula-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "chemformula-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "chemformula-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "chemformula-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "chemformula-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "chemformula-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "chemformula-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "chemformula-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "chemformula-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "chemformula-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "chemformula-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "chemformula-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "chemformula-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "chemformula-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "chemformula-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "chemformula-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "chemformula-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "chemformula-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "chemformula-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "chemformula-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "chemformula-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "chemformula-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "chemformula-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "chemformula-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "chemformula-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "chemformula-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "chemformula-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "chemformula-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "chemformula-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "chemformula-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "chemformula-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "chemformula-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "chemformula-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "chemformula-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "chemformula-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "chemformula-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "chemformula-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "chemformula-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "chemformula-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "chemformula-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "chemformula-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "chemformula-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "chemformula-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "chemformula-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "chemformula-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "chemformula-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "chemformula-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "chemformula-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "chemformula-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "chemformula-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "chemformula-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "chemformula-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "chemformula-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "chemformula-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "chemformula-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "chemformula-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "chemformula-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "chemformula-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "chemformula-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "chemformula-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "chemformula-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "chemformula-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "chemformula-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "chemformula-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "chemformula-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "chemformula-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "chemformula-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "chemformula-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "chemformula-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "chemformula-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "chemformula-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "chemformula-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "chemformula-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "chemformula-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "chemformula-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "chemformula-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "chemformula-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "chemformula-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "chemformula-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "chemformula-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "chemformula-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "chemformula-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "chemformula-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "chemformula-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "chemformula-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "chemformula-cmd", "score": 0.0058847868741168765}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "chemformula-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "chemformula-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "chemformula-cmd", "score": 0.18137737738638837}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "chemformula-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "chemformula-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "chemformula-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "chemformula-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "chemformula-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "chemformula-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chemformula-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chemformula-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chemformula-cmd", "score": 0.004719094298848707}, {"caption": "\\sfrac{}{}", "snippet": "\\sfrac{$1}{$2}", "meta": "chemformula-cmd", "score": 0.0030164694688453453}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemformula-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "chemformula-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "chemformula-cmd", "score": 0.0063276692758974925}], "pgfautomata": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfautomata-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfautomata-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfautomata-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfautomata-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfautomata-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfautomata-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfautomata-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfautomata-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfautomata-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfautomata-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfautomata-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfautomata-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfautomata-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfautomata-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfautomata-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfautomata-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfautomata-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfautomata-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfautomata-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfautomata-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfautomata-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfautomata-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfautomata-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfautomata-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfautomata-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfautomata-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfautomata-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfautomata-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfautomata-cmd", "score": 0.2864294797053033}], "pgfnodes": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfnodes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfnodes-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfnodes-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfnodes-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfnodes-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfnodes-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfnodes-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfnodes-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfnodes-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfnodes-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfnodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfnodes-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfnodes-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfnodes-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfnodes-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfnodes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfnodes-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfnodes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfnodes-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfnodes-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfnodes-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfnodes-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfnodes-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfnodes-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfnodes-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfnodes-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfnodes-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfnodes-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfnodes-cmd", "score": 0.2864294797053033}], "pgfarrows": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfarrows-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfarrows-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfarrows-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfarrows-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfarrows-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfarrows-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfarrows-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfarrows-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfarrows-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfarrows-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfarrows-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfarrows-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfarrows-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfarrows-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfarrows-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfarrows-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfarrows-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfarrows-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfarrows-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfarrows-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfarrows-cmd", "score": 0.2864294797053033}], "pst-text": [{"caption": "\\green", "snippet": "\\green", "meta": "pst-text-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pst-text-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pst-text-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pst-text-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pst-text-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pst-text-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pst-text-cmd", "score": 0.006520475264573554}], "keystroke": [{"caption": "\\csname", "snippet": "\\csname", "meta": "keystroke-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "keystroke-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "keystroke-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "keystroke-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "keystroke-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "keystroke-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "keystroke-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "keystroke-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "keystroke-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "keystroke-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "keystroke-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "keystroke-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "keystroke-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "keystroke-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "keystroke-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "keystroke-cmd", "score": 0.004649150613625593}], "currvita": [{"caption": "\\cvheadingfont", "snippet": "\\cvheadingfont", "meta": "currvita-cmd", "score": 5.547871753177405e-05}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "currvita-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "currvita-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "currvita-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "currvita-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "currvita-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "currvita-cmd", "score": 0.0018957469739775527}], "subfigmat": [{"caption": "\\subfigure[]{}", "snippet": "\\subfigure[$1]{$2}", "meta": "subfigmat-cmd", "score": 0.037856842641104005}, {"caption": "\\subref{}", "snippet": "\\subref{$1}", "meta": "subfigmat-cmd", "score": 0.007192033516871399}, {"caption": "\\subfigure[]{}", "snippet": "\\subfigure[$1]{$2}", "meta": "subfigmat-cmd", "score": 0.037856842641104005}], "boxhandler": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "boxhandler-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "boxhandler-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "boxhandler-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "boxhandler-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "boxhandler-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "boxhandler-cmd", "score": 0.0018957469739775527}, {"caption": "\\pbox{}{}", "snippet": "\\pbox{$1}{$2}", "meta": "boxhandler-cmd", "score": 0.0010883030320478486}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "boxhandler-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "boxhandler-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "boxhandler-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "boxhandler-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "boxhandler-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "boxhandler-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "boxhandler-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "boxhandler-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "boxhandler-cmd", "score": 0.028955796305270766}], "media9": [{"caption": "\\empty", "snippet": "\\empty", "meta": "media9-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "media9-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "media9-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "media9-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "media9-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "media9-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "media9-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "media9-cmd", "score": 0.2864294797053033}], "translator": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "translator-cmd", "score": 0.00037306820619479756}], "german": [{"caption": "\\today", "snippet": "\\today", "meta": "german-cmd", "score": 0.10733849317324783}], "mhsetup": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mhsetup-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mhsetup-cmd", "score": 0.021170869458413965}], "nomentbl": [{"caption": "\\nomenclature[]{}{}", "snippet": "\\nomenclature[$1]{$2}{$3}", "meta": "nomentbl-cmd", "score": 0.016053526743355948}, {"caption": "\\nomenclature{}{}", "snippet": "\\nomenclature{$1}{$2}", "meta": "nomentbl-cmd", "score": 0.016053526743355948}, {"caption": "\\nomlabel", "snippet": "\\nomlabel", "meta": "nomentbl-cmd", "score": 6.353668036093916e-05}, {"caption": "\\printnomenclature", "snippet": "\\printnomenclature", "meta": "nomentbl-cmd", "score": 0.0014526113324237952}, {"caption": "\\printnomenclature[]", "snippet": "\\printnomenclature[$1]", "meta": "nomentbl-cmd", "score": 0.0014526113324237952}, {"caption": "\\makenomenclature", "snippet": "\\makenomenclature", "meta": "nomentbl-cmd", "score": 0.002310610204652063}, {"caption": "\\nomgroup", "snippet": "\\nomgroup", "meta": "nomentbl-cmd", "score": 0.0005549290951493257}, {"caption": "\\nomgroup[]{}", "snippet": "\\nomgroup[$1]{$2}", "meta": "nomentbl-cmd", "score": 0.0005549290951493257}, {"caption": "\\nomname", "snippet": "\\nomname", "meta": "nomentbl-cmd", "score": 0.0015092617929470952}, {"caption": "\\nompreamble", "snippet": "\\nompreamble", "meta": "nomentbl-cmd", "score": 2.4350510995473236e-05}, {"caption": "\\nomentryend", "snippet": "\\nomentryend", "meta": "nomentbl-cmd", "score": 0.000137692304514793}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "nomentbl-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "nomentbl-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "nomentbl-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "nomentbl-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "nomentbl-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "nomentbl-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "nomentbl-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "nomentbl-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "nomentbl-cmd", "score": 0.028955796305270766}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "nomentbl-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "nomentbl-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "nomentbl-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "nomentbl-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "nomentbl-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "nomentbl-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "nomentbl-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "nomentbl-cmd", "score": 0.018615449342361392}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "nomentbl-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "nomentbl-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "nomentbl-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "nomentbl-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "nomentbl-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "nomentbl-cmd", "score": 0.0018957469739775527}, {"caption": "\\endhead", "snippet": "\\endhead", "meta": "nomentbl-cmd", "score": 0.0023853501147448834}, {"caption": "\\endfoot", "snippet": "\\endfoot", "meta": "nomentbl-cmd", "score": 0.00044045261916551967}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "nomentbl-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "nomentbl-cmd", "score": 0.021170869458413965}, {"caption": "\\nopagebreak", "snippet": "\\nopagebreak", "meta": "nomentbl-cmd", "score": 9.952664522415981e-05}, {"caption": "\\endfirsthead", "snippet": "\\endfirsthead", "meta": "nomentbl-cmd", "score": 0.0016148498709822416}, {"caption": "\\endlastfoot", "snippet": "\\endlastfoot", "meta": "nomentbl-cmd", "score": 0.00044045261916551967}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "nomentbl-cmd", "score": 0.3277033727934986}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "nomentbl-cmd", "score": 0.0029238994233674776}, {"caption": "\\pagebreak", "snippet": "\\pagebreak", "meta": "nomentbl-cmd", "score": 0.0313525090421608}], "miller": [{"caption": "\\hkl", "snippet": "\\hkl", "meta": "miller-cmd", "score": 0.0034259481311452946}, {"caption": "\\hkl{}", "snippet": "\\hkl{$1}", "meta": "miller-cmd", "score": 0.0034259481311452946}, {"caption": "\\hkl[]", "snippet": "\\hkl[$1]", "meta": "miller-cmd", "score": 0.0034259481311452946}], "lpform": [{"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "lpform-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "lpform-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "lpform-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "lpform-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "lpform-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "lpform-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "lpform-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "lpform-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "lpform-cmd", "score": 0.028955796305270766}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "lpform-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "lpform-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "lpform-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "lpform-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "lpform-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "lpform-cmd", "score": 0.0018957469739775527}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "lpform-cmd", "score": 0.01590723355124104}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "lpform-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "lpform-cmd", "score": 0.009331077109224957}], "xepersian": [{"caption": "\\settextfont[]{}", "snippet": "\\settextfont[$1]{$2}", "meta": "xepersian-cmd", "score": 0.00015447355412753335}, {"caption": "\\empty", "snippet": "\\empty", "meta": "xepersian-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xepersian-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xepersian-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "xepersian-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "xepersian-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "xepersian-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "xepersian-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xepersian-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "xepersian-cmd", "score": 0.002958865219480927}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xepersian-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xepersian-cmd", "score": 0.2864294797053033}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "xepersian-cmd", "score": 0.3277033727934986}, {"caption": "\\empty", "snippet": "\\empty", "meta": "xepersian-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "xepersian-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "xepersian-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xepersian-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "xepersian-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xepersian-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xepersian-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xepersian-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "xepersian-cmd", "score": 0.002958865219480927}], "chapterbib": [{"caption": "\\bibliographystyle{}", "snippet": "\\bibliographystyle{$1}", "meta": "chapterbib-cmd", "score": 0.25122317941387773}, {"caption": "\\bibliography{}", "snippet": "\\bibliography{$1}", "meta": "chapterbib-cmd", "score": 0.2659628337907604}, {"caption": "\\include{}", "snippet": "\\include{$1}", "meta": "chapterbib-cmd", "score": 0.1547080054979312}], "scalerel": [{"caption": "\\scaleto{}{}", "snippet": "\\scaleto{$1}{$2}", "meta": "scalerel-cmd", "score": 0.00027615383978106523}, {"caption": "\\csname", "snippet": "\\csname", "meta": "scalerel-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "scalerel-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "scalerel-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "scalerel-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "scalerel-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "scalerel-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "scalerel-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "scalerel-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "scalerel-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "scalerel-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "scalerel-cmd", "score": 0.028955796305270766}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "scalerel-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "scalerel-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "scalerel-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "scalerel-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "scalerel-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "scalerel-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "scalerel-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "scalerel-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "scalerel-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "scalerel-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "scalerel-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "scalerel-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "scalerel-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "scalerel-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "scalerel-cmd", "score": 0.004649150613625593}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "scalerel-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "scalerel-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "scalerel-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "scalerel-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "scalerel-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "scalerel-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "scalerel-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "scalerel-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "scalerel-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "scalerel-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "scalerel-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "scalerel-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "scalerel-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "scalerel-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "scalerel-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "scalerel-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "scalerel-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "scalerel-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "scalerel-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "scalerel-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "scalerel-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "scalerel-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "scalerel-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "scalerel-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "scalerel-cmd", "score": 0.004719094298848707}], "extarrows": [{"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "extarrows-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "extarrows-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "extarrows-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "extarrows-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "extarrows-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "extarrows-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "extarrows-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "extarrows-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "extarrows-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "extarrows-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "extarrows-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "extarrows-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "extarrows-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "extarrows-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "extarrows-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "extarrows-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "extarrows-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "extarrows-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "extarrows-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "extarrows-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "extarrows-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "extarrows-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "extarrows-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "extarrows-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "extarrows-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "extarrows-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "extarrows-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "extarrows-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "extarrows-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "extarrows-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "extarrows-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "extarrows-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "extarrows-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "extarrows-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "extarrows-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "extarrows-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "extarrows-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "extarrows-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "extarrows-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "extarrows-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "extarrows-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "extarrows-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "extarrows-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "extarrows-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "extarrows-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "extarrows-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "extarrows-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "extarrows-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "extarrows-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "extarrows-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "extarrows-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "extarrows-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "extarrows-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "extarrows-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "extarrows-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "extarrows-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "extarrows-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "extarrows-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "extarrows-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "extarrows-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "extarrows-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "extarrows-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "extarrows-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "extarrows-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "extarrows-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "extarrows-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "extarrows-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "extarrows-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "extarrows-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "extarrows-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "extarrows-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "extarrows-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "extarrows-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "extarrows-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "extarrows-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "extarrows-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "extarrows-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "extarrows-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "extarrows-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "extarrows-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "extarrows-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "extarrows-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "extarrows-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "extarrows-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "extarrows-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "extarrows-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "extarrows-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "extarrows-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "extarrows-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "extarrows-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "extarrows-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "extarrows-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "extarrows-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "extarrows-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "extarrows-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "extarrows-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "extarrows-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "extarrows-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "extarrows-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "extarrows-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "extarrows-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "extarrows-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "extarrows-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "extarrows-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "extarrows-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "extarrows-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "extarrows-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "extarrows-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "extarrows-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "extarrows-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "extarrows-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "extarrows-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "extarrows-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "extarrows-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "extarrows-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "extarrows-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "extarrows-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "extarrows-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "extarrows-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "extarrows-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "extarrows-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "extarrows-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "extarrows-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "extarrows-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "extarrows-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "extarrows-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "extarrows-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "extarrows-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "extarrows-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "extarrows-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "extarrows-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "extarrows-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "extarrows-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "extarrows-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "extarrows-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "extarrows-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "extarrows-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "extarrows-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "extarrows-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "extarrows-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "extarrows-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "extarrows-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "extarrows-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "extarrows-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "extarrows-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "extarrows-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "extarrows-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "extarrows-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "extarrows-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "extarrows-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "extarrows-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "extarrows-cmd", "score": 0.0063276692758974925}], "listingsutf8": [{"caption": "\\vskip", "snippet": "\\vskip", "meta": "listingsutf8-cmd", "score": 0.05143052892347224}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "listingsutf8-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "listingsutf8-cmd", "score": 0.021170869458413965}, {"caption": "\\do", "snippet": "\\do", "meta": "listingsutf8-cmd", "score": 0.009278344180101056}, {"caption": "\\thelstlisting", "snippet": "\\thelstlisting", "meta": "listingsutf8-cmd", "score": 0.00012774128088872144}, {"caption": "\\lstinputlisting[]{}", "snippet": "\\lstinputlisting[$1]{$2}", "meta": "listingsutf8-cmd", "score": 0.011660477607086044}, {"caption": "\\lstinputlisting{}", "snippet": "\\lstinputlisting{$1}", "meta": "listingsutf8-cmd", "score": 0.011660477607086044}, {"caption": "\\space", "snippet": "\\space", "meta": "listingsutf8-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "listingsutf8-cmd", "score": 0.008565354665444157}, {"caption": "\\lstinline", "snippet": "\\lstinline", "meta": "listingsutf8-cmd", "score": 0.005972262850694285}, {"caption": "\\lstinline{}", "snippet": "\\lstinline{$1}", "meta": "listingsutf8-cmd", "score": 0.005972262850694285}, {"caption": "\\lstlistoflistings", "snippet": "\\lstlistoflistings", "meta": "listingsutf8-cmd", "score": 0.005279080363360602}, {"caption": "\\csname", "snippet": "\\csname", "meta": "listingsutf8-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "listingsutf8-cmd", "score": 0.002958865219480927}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "listingsutf8-cmd", "score": 0.00037306820619479756}], "forloop": [{"caption": "\\forloop{}{}{}{}", "snippet": "\\forloop{$1}{$2}{$3}{$4}", "meta": "forloop-cmd", "score": 0.00029867998381154486}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "forloop-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "forloop-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "forloop-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "forloop-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "forloop-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "forloop-cmd", "score": 0.0018957469739775527}], "xymtex": [{"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "xymtex-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "xymtex-cmd", "score": 0.022224283488673075}, {"caption": "\\mathcal{}", "snippet": "\\mathcal{$1}", "meta": "xymtex-cmd", "score": 0.35084018920966636}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "xymtex-cmd", "score": 0.002995924112493351}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "xymtex-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xymtex-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xymtex-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "xymtex-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "xymtex-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "xymtex-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "xymtex-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "xymtex-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xymtex-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "xymtex-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "xymtex-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xymtex-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "xymtex-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "xymtex-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xymtex-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xymtex-cmd", "score": 0.2864294797053033}], "eqlist": [{"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "eqlist-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "eqlist-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "eqlist-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "eqlist-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "eqlist-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "eqlist-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "eqlist-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "eqlist-cmd", "score": 0.018615449342361392}, {"caption": "\\csname", "snippet": "\\csname", "meta": "eqlist-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "eqlist-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "eqlist-cmd", "score": 0.021170869458413965}, {"caption": "\\eqparbox{}{}", "snippet": "\\eqparbox{$1}{$2}", "meta": "eqlist-cmd", "score": 2.9423534119530166e-05}, {"caption": "\\item", "snippet": "\\item", "meta": "eqlist-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "eqlist-cmd", "score": 3.800886892251021}], "tgschola": [{"caption": "\\empty", "snippet": "\\empty", "meta": "tgschola-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgschola-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgschola-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgschola-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgschola-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgschola-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgschola-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgschola-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tgschola-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tgschola-cmd", "score": 0.021170869458413965}], "mfirstuc": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "mfirstuc-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "mfirstuc-cmd", "score": 0.021170869458413965}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "mfirstuc-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "mfirstuc-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "mfirstuc-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "mfirstuc-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "mfirstuc-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "mfirstuc-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "mfirstuc-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "mfirstuc-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "mfirstuc-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "mfirstuc-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "mfirstuc-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "mfirstuc-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "mfirstuc-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "mfirstuc-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "mfirstuc-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "mfirstuc-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "mfirstuc-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "mfirstuc-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "mfirstuc-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "mfirstuc-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "mfirstuc-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "mfirstuc-cmd", "score": 0.008565354665444157}], "gloss": [{"caption": "\\makegloss", "snippet": "\\makegloss", "meta": "gloss-cmd", "score": 0.0018653410309739879}], "ltxcmds": [{"caption": "\\csname", "snippet": "\\csname", "meta": "ltxcmds-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "ltxcmds-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "ltxcmds-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "ltxcmds-cmd", "score": 0.021170869458413965}], "outlines": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "outlines-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "outlines-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "outlines-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "outlines-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "outlines-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "outlines-cmd", "score": 0.0018957469739775527}], "typearea": [{"caption": "\\newpage", "snippet": "\\newpage", "meta": "typearea-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "typearea-cmd", "score": 0.1789117552185788}, {"caption": "\\addtokomafont{}{}", "snippet": "\\addtokomafont{$1}{$2}", "meta": "typearea-cmd", "score": 0.0008555564394100388}, {"caption": "\\setkomafont{}{}", "snippet": "\\setkomafont{$1}{$2}", "meta": "typearea-cmd", "score": 0.012985816912639263}, {"caption": "\\KOMAoptions{}", "snippet": "\\KOMAoptions{$1}", "meta": "typearea-cmd", "score": 0.000396664302361659}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "typearea-cmd", "score": 0.00037306820619479756}], "currfile": [{"caption": "\\currfiledir", "snippet": "\\currfiledir", "meta": "currfile-cmd", "score": 0.0002459788020229296}, {"caption": "\\empty", "snippet": "\\empty", "meta": "currfile-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "currfile-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "currfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "currfile-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "currfile-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "currfile-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "currfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "currfile-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "currfile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "currfile-cmd", "score": 0.021170869458413965}], "toptesi": [{"caption": "\\tomo", "snippet": "\\tomo", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\mainmatter", "snippet": "\\mainmatter", "meta": "toptesi-cmd", "score": 0.025705092792367497}, {"caption": "\\ringraziamenti", "snippet": "\\ringraziamenti", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\sommario", "snippet": "\\sommario", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\NoteWhiteLine", "snippet": "\\NoteWhiteLine", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\paginavuota", "snippet": "\\paginavuota", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\nota{}", "snippet": "\\nota{$1}", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\indici", "snippet": "\\indici", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\csname", "snippet": "\\csname", "meta": "toptesi-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "toptesi-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "toptesi-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "toptesi-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "toptesi-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "toptesi-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "toptesi-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "toptesi-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "toptesi-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "toptesi-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "toptesi-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "toptesi-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "toptesi-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "toptesi-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "toptesi-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "toptesi-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "toptesi-cmd", "score": 0.004649150613625593}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "toptesi-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "toptesi-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "toptesi-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "toptesi-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "toptesi-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "toptesi-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "toptesi-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "toptesi-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "toptesi-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "toptesi-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "toptesi-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "toptesi-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "toptesi-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "toptesi-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "toptesi-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "toptesi-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "toptesi-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "toptesi-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "toptesi-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "toptesi-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "toptesi-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "toptesi-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "toptesi-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "toptesi-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "toptesi-cmd", "score": 0.004719094298848707}, {"caption": "\\listing{}", "snippet": "\\listing{$1}", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\micro", "snippet": "\\micro", "meta": "toptesi-cmd", "score": 0.011051971930487929}, {"caption": "\\gradi", "snippet": "\\gradi", "meta": "toptesi-cmd", "score": 0.00023765162173466673}, {"caption": "\\unit[]{}", "snippet": "\\unit[$1]{$2}", "meta": "toptesi-cmd", "score": 0.028299796173135428}, {"caption": "\\unit{}", "snippet": "\\unit{$1}", "meta": "toptesi-cmd", "score": 0.028299796173135428}, {"caption": "\\ped{}", "snippet": "\\ped{$1}", "meta": "toptesi-cmd", "score": 0.0007129548652040002}, {"caption": "\\ohm", "snippet": "\\ohm", "meta": "toptesi-cmd", "score": 0.0038146685721293138}, {"caption": "\\gei", "snippet": "\\gei", "meta": "toptesi-cmd", "score": 0.00023765162173466673}], "amsrefs": [{"caption": "\\ndash", "snippet": "\\ndash", "meta": "amsrefs-cmd", "score": 0.0003420867634658178}, {"caption": "\\bib{}{}{}", "snippet": "\\bib{$1}{$2}{$3}", "meta": "amsrefs-cmd", "score": 0.0017473230242849183}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "amsrefs-cmd", "score": 2.341195220791228}], "sistyle": [{"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "sistyle-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "sistyle-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "sistyle-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "sistyle-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "sistyle-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "sistyle-cmd", "score": 0.0063276692758974925}], "suffix": [{"caption": "\\let", "snippet": "\\let", "meta": "suffix-cmd", "score": 0.03789745970461662}], "sansmath": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "sansmath-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "sansmath-cmd", "score": 0.021170869458413965}], "tikz-qtree": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-qtree-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-qtree-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikz-qtree-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikz-qtree-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikz-qtree-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikz-qtree-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-qtree-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikz-qtree-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-qtree-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikz-qtree-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-qtree-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-qtree-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikz-qtree-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-qtree-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-qtree-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-qtree-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikz-qtree-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-qtree-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-qtree-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikz-qtree-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikz-qtree-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikz-qtree-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-qtree-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikz-qtree-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-qtree-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikz-qtree-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikz-qtree-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikz-qtree-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikz-qtree-cmd", "score": 0.2864294797053033}], "floatpag": [{"caption": "\\rotfloatpagestyle{}", "snippet": "\\rotfloatpagestyle{$1}", "meta": "floatpag-cmd", "score": 0.0004535003423927585}, {"caption": "\\floatpagestyle{}", "snippet": "\\floatpagestyle{$1}", "meta": "floatpag-cmd", "score": 0.0004535003423927585}], "colortab": [{"caption": "\\shadowbox{}", "snippet": "\\shadowbox{$1}", "meta": "colortab-cmd", "score": 0.00107667147399019}, {"caption": "\\doublebox", "snippet": "\\doublebox", "meta": "colortab-cmd", "score": 0.00015142240898356106}, {"caption": "\\VerbatimEnvironment", "snippet": "\\VerbatimEnvironment", "meta": "colortab-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\thisfancypage{}{}", "snippet": "\\thisfancypage{$1}{$2}", "meta": "colortab-cmd", "score": 0.00015142240898356106}, {"caption": "\\TheSbox", "snippet": "\\TheSbox", "meta": "colortab-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\green", "snippet": "\\green", "meta": "colortab-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "colortab-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "colortab-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "colortab-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "colortab-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "colortab-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "colortab-cmd", "score": 0.006520475264573554}], "parcolumns": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "parcolumns-cmd", "score": 0.00037306820619479756}], "dingbat": [{"caption": "\\checkmark", "snippet": "\\checkmark", "meta": "dingbat-cmd", "score": 0.025060530944368123}], "ifoddpage": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "ifoddpage-cmd", "score": 0.00530510025314411}, {"caption": "\\checkoddpage", "snippet": "\\checkoddpage", "meta": "ifoddpage-cmd", "score": 0.00028672585452906425}], "kvoptions": [{"caption": "\\empty", "snippet": "\\empty", "meta": "kvoptions-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "kvoptions-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "kvoptions-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "kvoptions-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "kvoptions-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "kvoptions-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "kvoptions-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "kvoptions-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "kvoptions-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "kvoptions-cmd", "score": 0.021170869458413965}], "pst-tree": [{"caption": "\\green", "snippet": "\\green", "meta": "pst-tree-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pst-tree-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pst-tree-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pst-tree-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pst-tree-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pst-tree-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pst-tree-cmd", "score": 0.006520475264573554}], "nonfloat": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "nonfloat-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "nonfloat-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "nonfloat-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "nonfloat-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "nonfloat-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "nonfloat-cmd", "score": 0.0018957469739775527}], "rsphrase": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "rsphrase-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "rsphrase-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "rsphrase-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "rsphrase-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "rsphrase-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "rsphrase-cmd", "score": 0.0018957469739775527}], "beramono": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "beramono-cmd", "score": 0.00037306820619479756}], "pgfbaseimage": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfbaseimage-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfbaseimage-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfbaseimage-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfbaseimage-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfbaseimage-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfbaseimage-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfbaseimage-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfbaseimage-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfbaseimage-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfbaseimage-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfbaseimage-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfbaseimage-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfbaseimage-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfbaseimage-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfbaseimage-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfbaseimage-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfbaseimage-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfbaseimage-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfbaseimage-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfbaseimage-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfbaseimage-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfbaseimage-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfbaseimage-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfbaseimage-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfbaseimage-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfbaseimage-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfbaseimage-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfbaseimage-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfbaseimage-cmd", "score": 0.2864294797053033}], "romannum": [{"caption": "\\thefootnote", "snippet": "\\thefootnote", "meta": "romannum-cmd", "score": 0.007676927812687567}, {"caption": "\\thefootnote{}", "snippet": "\\thefootnote{$1}", "meta": "romannum-cmd", "score": 0.007676927812687567}], "tgtermes": [{"caption": "\\empty", "snippet": "\\empty", "meta": "tgtermes-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgtermes-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgtermes-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgtermes-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgtermes-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgtermes-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgtermes-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgtermes-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tgtermes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tgtermes-cmd", "score": 0.021170869458413965}], "Alegreya": [{"caption": "\\rmfamily", "snippet": "\\rmfamily", "meta": "Alegreya-cmd", "score": 0.00898937903263608}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "Alegreya-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "Alegreya-cmd", "score": 0.008565354665444157}], "glossaries-extra": [{"caption": "\\gls{}", "snippet": "\\gls{$1}", "meta": "glossaries-extra-cmd", "score": 0.06939353309055077}, {"caption": "\\Gls{}", "snippet": "\\Gls{$1}", "meta": "glossaries-extra-cmd", "score": 0.003696678698317109}, {"caption": "\\makeglossaries", "snippet": "\\makeglossaries", "meta": "glossaries-extra-cmd", "score": 0.0056737600836936995}, {"caption": "\\newabbreviation{}{}{}", "snippet": "\\newabbreviation{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 0.00023275591440052114}, {"caption": "\\newglossaryentry{}{}", "snippet": "\\newglossaryentry{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.018524394136900962}, {"caption": "\\newglossary{}{}", "snippet": "\\newglossary{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 1.4547244650032571e-05}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "glossaries-extra-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "glossaries-extra-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "glossaries-extra-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "glossaries-extra-cmd", "score": 0.021170869458413965}, {"caption": "\\glslongpluralkey", "snippet": "\\glslongpluralkey", "meta": "glossaries-extra-cmd", "score": 1.4538687447297259e-05}, {"caption": "\\Glspl{}", "snippet": "\\Glspl{$1}", "meta": "glossaries-extra-cmd", "score": 0.0025291265119320736}, {"caption": "\\glossarysection", "snippet": "\\glossarysection", "meta": "glossaries-extra-cmd", "score": 9.579755294730752e-05}, {"caption": "\\printglossaries", "snippet": "\\printglossaries", "meta": "glossaries-extra-cmd", "score": 0.0010106582768889887}, {"caption": "\\Gls{}", "snippet": "\\Gls{$1}", "meta": "glossaries-extra-cmd", "score": 0.003696678698317109}, {"caption": "\\setglossarystyle{}", "snippet": "\\setglossarystyle{$1}", "meta": "glossaries-extra-cmd", "score": 0.0003758893277679221}, {"caption": "\\printglossary", "snippet": "\\printglossary", "meta": "glossaries-extra-cmd", "score": 0.009139682306158714}, {"caption": "\\printglossary[]", "snippet": "\\printglossary[$1]", "meta": "glossaries-extra-cmd", "score": 0.009139682306158714}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-extra-cmd", "score": 0.009278344180101056}, {"caption": "\\setglossarysection{}", "snippet": "\\setglossarysection{$1}", "meta": "glossaries-extra-cmd", "score": 3.6081414102781514e-05}, {"caption": "\\glsresetall", "snippet": "\\glsresetall", "meta": "glossaries-extra-cmd", "score": 0.0006123462672467326}, {"caption": "\\the", "snippet": "\\the", "meta": "glossaries-extra-cmd", "score": 0.007238960303946444}, {"caption": "\\acrshort{}", "snippet": "\\acrshort{$1}", "meta": "glossaries-extra-cmd", "score": 0.009936841864059727}, {"caption": "\\printnoidxglossary[]", "snippet": "\\printnoidxglossary[$1]", "meta": "glossaries-extra-cmd", "score": 0.00021912375285685037}, {"caption": "\\newglossary{}{}", "snippet": "\\newglossary{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 1.4547244650032571e-05}, {"caption": "\\gls{}", "snippet": "\\gls{$1}", "meta": "glossaries-extra-cmd", "score": 0.06939353309055077}, {"caption": "\\printnoidxglossaries", "snippet": "\\printnoidxglossaries", "meta": "glossaries-extra-cmd", "score": 5.6789564226023136e-05}, {"caption": "\\printindex", "snippet": "\\printindex", "meta": "glossaries-extra-cmd", "score": 0.004417016910870522}, {"caption": "\\defglsentryfmt[]{}", "snippet": "\\defglsentryfmt[$1]{$2}", "meta": "glossaries-extra-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\glspostdescription", "snippet": "\\glspostdescription", "meta": "glossaries-extra-cmd", "score": 0.0006337376579591112}, {"caption": "\\number", "snippet": "\\number", "meta": "glossaries-extra-cmd", "score": 0.000968714260809983}, {"caption": "\\glsaddall", "snippet": "\\glsaddall", "meta": "glossaries-extra-cmd", "score": 0.0008363820557740373}, {"caption": "\\glsaddall[]", "snippet": "\\glsaddall[$1]", "meta": "glossaries-extra-cmd", "score": 0.0008363820557740373}, {"caption": "\\makeglossaries", "snippet": "\\makeglossaries", "meta": "glossaries-extra-cmd", "score": 0.0056737600836936995}, {"caption": "\\glossaryname", "snippet": "\\glossaryname", "meta": "glossaries-extra-cmd", "score": 0.0006174536302752427}, {"caption": "\\newglossaryentry{}{}", "snippet": "\\newglossaryentry{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.018524394136900962}, {"caption": "\\glslabel", "snippet": "\\glslabel", "meta": "glossaries-extra-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\glsadd{}", "snippet": "\\glsadd{$1}", "meta": "glossaries-extra-cmd", "score": 3.0150373480213892e-05}, {"caption": "\\makenoidxglossaries", "snippet": "\\makenoidxglossaries", "meta": "glossaries-extra-cmd", "score": 0.0001382210125680805}, {"caption": "\\glsgenentryfmt", "snippet": "\\glsgenentryfmt", "meta": "glossaries-extra-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\acronymtype", "snippet": "\\acronymtype", "meta": "glossaries-extra-cmd", "score": 0.002000834271117562}, {"caption": "\\acrfull{}", "snippet": "\\acrfull{$1}", "meta": "glossaries-extra-cmd", "score": 0.0032622587277765067}, {"caption": "\\newacronym{}{}{}", "snippet": "\\newacronym{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 0.03193935544723102}, {"caption": "\\glspl{}", "snippet": "\\glspl{$1}", "meta": "glossaries-extra-cmd", "score": 0.0034025897522047717}, {"caption": "\\ifglsused{}{}{}", "snippet": "\\ifglsused{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 4.8990621725283124e-05}, {"caption": "\\acrlong{}", "snippet": "\\acrlong{$1}", "meta": "glossaries-extra-cmd", "score": 0.002517821598213752}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "glossaries-extra-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-extra-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "glossaries-extra-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "glossaries-extra-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "glossaries-extra-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "glossaries-extra-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "glossaries-extra-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "glossaries-extra-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "glossaries-extra-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "glossaries-extra-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "glossaries-extra-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "glossaries-extra-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "glossaries-extra-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "glossaries-extra-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "glossaries-extra-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "glossaries-extra-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "glossaries-extra-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "glossaries-extra-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "glossaries-extra-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "glossaries-extra-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "glossaries-extra-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "glossaries-extra-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "glossaries-extra-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "glossaries-extra-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "glossaries-extra-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "glossaries-extra-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "glossaries-extra-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "glossaries-extra-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "glossaries-extra-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "glossaries-extra-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "glossaries-extra-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "glossaries-extra-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "glossaries-extra-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "glossaries-extra-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "glossaries-extra-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "glossaries-extra-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "glossaries-extra-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "glossaries-extra-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "glossaries-extra-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "glossaries-extra-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "glossaries-extra-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "glossaries-extra-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "glossaries-extra-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "glossaries-extra-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "glossaries-extra-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "glossaries-extra-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "glossaries-extra-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "glossaries-extra-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "glossaries-extra-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "glossaries-extra-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "glossaries-extra-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "glossaries-extra-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "glossaries-extra-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "glossaries-extra-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "glossaries-extra-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "glossaries-extra-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "glossaries-extra-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "glossaries-extra-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "glossaries-extra-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "glossaries-extra-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "glossaries-extra-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "glossaries-extra-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "glossaries-extra-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "glossaries-extra-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "glossaries-extra-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "glossaries-extra-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "glossaries-extra-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "glossaries-extra-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "glossaries-extra-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "glossaries-extra-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "glossaries-extra-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "glossaries-extra-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "glossaries-extra-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "glossaries-extra-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "glossaries-extra-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "glossaries-extra-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "glossaries-extra-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "glossaries-extra-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "glossaries-extra-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "glossaries-extra-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "glossaries-extra-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "glossaries-extra-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "glossaries-extra-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "glossaries-extra-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "glossaries-extra-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "glossaries-extra-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "glossaries-extra-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "glossaries-extra-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "glossaries-extra-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "glossaries-extra-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "glossaries-extra-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "glossaries-extra-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "glossaries-extra-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "glossaries-extra-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "glossaries-extra-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "glossaries-extra-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "glossaries-extra-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "glossaries-extra-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "glossaries-extra-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "glossaries-extra-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "glossaries-extra-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "glossaries-extra-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "glossaries-extra-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "glossaries-extra-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "glossaries-extra-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "glossaries-extra-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "glossaries-extra-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "glossaries-extra-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "glossaries-extra-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "glossaries-extra-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "glossaries-extra-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "glossaries-extra-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "glossaries-extra-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "glossaries-extra-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "glossaries-extra-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "glossaries-extra-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "glossaries-extra-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "glossaries-extra-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "glossaries-extra-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "glossaries-extra-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "glossaries-extra-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "glossaries-extra-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "glossaries-extra-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "glossaries-extra-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "glossaries-extra-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "glossaries-extra-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "glossaries-extra-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "glossaries-extra-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "glossaries-extra-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "glossaries-extra-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "glossaries-extra-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "glossaries-extra-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "glossaries-extra-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "glossaries-extra-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "glossaries-extra-cmd", "score": 0.008565354665444157}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "glossaries-extra-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "glossaries-extra-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "glossaries-extra-cmd", "score": 0.18137737738638837}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "glossaries-extra-cmd", "score": 2.341195220791228}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "glossaries-extra-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "glossaries-extra-cmd", "score": 0.021170869458413965}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "glossaries-extra-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "glossaries-extra-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "glossaries-extra-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "glossaries-extra-cmd", "score": 0.0018957469739775527}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "glossaries-extra-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "glossaries-extra-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "glossaries-extra-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "glossaries-extra-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-extra-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "glossaries-extra-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "glossaries-extra-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "glossaries-extra-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "glossaries-extra-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "glossaries-extra-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "glossaries-extra-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "glossaries-extra-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "glossaries-extra-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "glossaries-extra-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "glossaries-extra-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "glossaries-extra-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "glossaries-extra-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "glossaries-extra-cmd", "score": 0.0063276692758974925}], "dashrule": [{"caption": "\\hdashrule[]{}{}{}", "snippet": "\\hdashrule[$1]{$2}{$3}{$4}", "meta": "dashrule-cmd", "score": 0.00029867998381154486}], "bclogo": [{"caption": "\\csname", "snippet": "\\csname", "meta": "bclogo-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "bclogo-cmd", "score": 0.00037306820619479756}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "bclogo-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "bclogo-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "bclogo-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "bclogo-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "bclogo-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "bclogo-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "bclogo-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "bclogo-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "bclogo-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "bclogo-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "bclogo-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "bclogo-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "bclogo-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "bclogo-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "bclogo-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "bclogo-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bclogo-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "bclogo-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "bclogo-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "bclogo-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "bclogo-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bclogo-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bclogo-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bclogo-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "bclogo-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "bclogo-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "bclogo-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "bclogo-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "bclogo-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bclogo-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "bclogo-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bclogo-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "bclogo-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "bclogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "bclogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "bclogo-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "bclogo-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "bclogo-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "bclogo-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "bclogo-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "bclogo-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "bclogo-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "bclogo-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "bclogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "bclogo-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "bclogo-cmd", "score": 0.004719094298848707}], "isomath": [{"caption": "\\empty", "snippet": "\\empty", "meta": "isomath-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "isomath-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "isomath-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "isomath-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "isomath-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "isomath-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "isomath-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "isomath-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "isomath-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "isomath-cmd", "score": 0.021170869458413965}], "tkz-graph": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-graph-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-graph-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tkz-graph-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tkz-graph-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tkz-graph-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tkz-graph-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-graph-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tkz-graph-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-graph-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tkz-graph-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-graph-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-graph-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tkz-graph-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "tkz-graph-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "tkz-graph-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "tkz-graph-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "tkz-graph-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "tkz-graph-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-graph-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-graph-cmd", "score": 0.004719094298848707}, {"caption": "\\reserveinserts{}", "snippet": "\\reserveinserts{$1}", "meta": "tkz-graph-cmd", "score": 0.0018653410309739879}, {"caption": "\\newtoks", "snippet": "\\newtoks", "meta": "tkz-graph-cmd", "score": 0.00031058155311734754}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-graph-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tkz-graph-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-graph-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-graph-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tkz-graph-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tkz-graph-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tkz-graph-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-graph-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tkz-graph-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-graph-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tkz-graph-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tkz-graph-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tkz-graph-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tkz-graph-cmd", "score": 0.2864294797053033}], "sourcesanspro": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "sourcesanspro-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "sourcesanspro-cmd", "score": 0.008565354665444157}], "longdivision": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "longdivision-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "longdivision-cmd", "score": 0.2864294797053033}], "xmpmulti": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "xmpmulti-cmd", "score": 0.00037306820619479756}], "epsdice": [{"caption": "\\csname", "snippet": "\\csname", "meta": "epsdice-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "epsdice-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "epsdice-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "epsdice-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "epsdice-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "epsdice-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "epsdice-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "epsdice-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "epsdice-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "epsdice-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "epsdice-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "epsdice-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "epsdice-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "epsdice-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "epsdice-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "epsdice-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "epsdice-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "epsdice-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "epsdice-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "epsdice-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "epsdice-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "epsdice-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "epsdice-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "epsdice-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "epsdice-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "epsdice-cmd", "score": 0.004719094298848707}], "apptools": [{"caption": "\\appendix", "snippet": "\\appendix", "meta": "apptools-cmd", "score": 0.047007158741781095}, {"caption": "\\AtAppendix{}", "snippet": "\\AtAppendix{$1}", "meta": "apptools-cmd", "score": 8.82390883984482e-06}], "letltxmacro": [{"caption": "\\csname", "snippet": "\\csname", "meta": "letltxmacro-cmd", "score": 0.008565354665444157}], "menukeys": [{"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "menukeys-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "menukeys-cmd", "score": 0.354445763583904}, {"caption": "\\adjustbox{}{}", "snippet": "\\adjustbox{$1}{$2}", "meta": "menukeys-cmd", "score": 0.002008185536556013}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "menukeys-cmd", "score": 0.00037306820619479756}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "menukeys-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "menukeys-cmd", "score": 1.4425339817971206}, {"caption": "\\usepackage{}", "snippet": "\\usepackage{$1}", "meta": "menukeys-cmd", "score": 5.427890758130527}, {"caption": "\\usepackage[]{}", "snippet": "\\usepackage[$1]{$2}", "meta": "menukeys-cmd", "score": 5.427890758130527}, {"caption": "\\empty", "snippet": "\\empty", "meta": "menukeys-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "menukeys-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "menukeys-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "menukeys-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "menukeys-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "menukeys-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "menukeys-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "menukeys-cmd", "score": 0.2864294797053033}, {"caption": "\\csname", "snippet": "\\csname", "meta": "menukeys-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "menukeys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "menukeys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "menukeys-cmd", "score": 0.004719094298848707}, {"caption": "\\mathlarger{}", "snippet": "\\mathlarger{$1}", "meta": "menukeys-cmd", "score": 0.0031475241540308316}, {"caption": "\\smaller", "snippet": "\\smaller", "meta": "menukeys-cmd", "score": 0.001271007880944704}, {"caption": "\\csname", "snippet": "\\csname", "meta": "menukeys-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "menukeys-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "menukeys-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "menukeys-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "menukeys-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "menukeys-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "menukeys-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "menukeys-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "menukeys-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "menukeys-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "menukeys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "menukeys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "menukeys-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "menukeys-cmd", "score": 0.004649150613625593}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "menukeys-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "menukeys-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "menukeys-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "menukeys-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "menukeys-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "menukeys-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "menukeys-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "menukeys-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "menukeys-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "menukeys-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "menukeys-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "menukeys-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "menukeys-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "menukeys-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "menukeys-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "menukeys-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "menukeys-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "menukeys-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "menukeys-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "menukeys-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "menukeys-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "menukeys-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "menukeys-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "menukeys-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "menukeys-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "menukeys-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "menukeys-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "menukeys-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "menukeys-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "menukeys-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "menukeys-cmd", "score": 0.2864294797053033}], "hypdvips": [{"caption": "\\begin{}", "snippet": "\\begin{$1}", "meta": "hypdvips-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}[]", "snippet": "\\begin{$1}[$2]", "meta": "hypdvips-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}{}", "snippet": "\\begin{$1}{$2}", "meta": "hypdvips-cmd", "score": 7.849662248028187}, {"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "hypdvips-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "hypdvips-cmd", "score": 0.8973590434087177}, {"caption": "\\title{}", "snippet": "\\title{$1}", "meta": "hypdvips-cmd", "score": 0.9202908262245683}, {"caption": "\\end{}", "snippet": "\\end{$1}", "meta": "hypdvips-cmd", "score": 7.847906405228455}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "hypdvips-cmd", "score": 0.1789117552185788}, {"caption": "\\global", "snippet": "\\global", "meta": "hypdvips-cmd", "score": 0.006609629561859019}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.00037306820619479756}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "hypdvips-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "hypdvips-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "hypdvips-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "hypdvips-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "hypdvips-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "hypdvips-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "hypdvips-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "hypdvips-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "hypdvips-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "hypdvips-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "hypdvips-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "hypdvips-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "hypdvips-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\nameref{}", "snippet": "\\nameref{$1}", "meta": "hypdvips-cmd", "score": 0.009472569279662113}, {"caption": "\\pdfbookmark[]{}{}", "snippet": "\\pdfbookmark[$1]{$2}{$3}", "meta": "hypdvips-cmd", "score": 0.006492248863367502}, {"caption": "\\figureautorefname", "snippet": "\\figureautorefname", "meta": "hypdvips-cmd", "score": 0.00014582556188448738}, {"caption": "\\figureautorefname{}", "snippet": "\\figureautorefname{$1}", "meta": "hypdvips-cmd", "score": 0.00014582556188448738}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.006963729684667191}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\footnoteautorefname", "snippet": "\\footnoteautorefname", "meta": "hypdvips-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\roman{}", "snippet": "\\roman{$1}", "meta": "hypdvips-cmd", "score": 0.005553384455935491}, {"caption": "\\roman", "snippet": "\\roman", "meta": "hypdvips-cmd", "score": 0.005553384455935491}, {"caption": "\\string", "snippet": "\\string", "meta": "hypdvips-cmd", "score": 0.001042697111754002}, {"caption": "\\MakeLowercase{}", "snippet": "\\MakeLowercase{$1}", "meta": "hypdvips-cmd", "score": 0.017289599800633146}, {"caption": "\\textunderscore", "snippet": "\\textunderscore", "meta": "hypdvips-cmd", "score": 0.001509072212764015}, {"caption": "\\do", "snippet": "\\do", "meta": "hypdvips-cmd", "score": 0.009278344180101056}, {"caption": "\\begin{}", "snippet": "\\begin{$1}", "meta": "hypdvips-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}[]", "snippet": "\\begin{$1}[$2]", "meta": "hypdvips-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}{}", "snippet": "\\begin{$1}{$2}", "meta": "hypdvips-cmd", "score": 7.849662248028187}, {"caption": "\\FancyVerbLineautorefname", "snippet": "\\FancyVerbLineautorefname", "meta": "hypdvips-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\hyperlink{}{}", "snippet": "\\hyperlink{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.00978652043902115}, {"caption": "\\tableautorefname", "snippet": "\\tableautorefname", "meta": "hypdvips-cmd", "score": 0.00012704528567339081}, {"caption": "\\tableautorefname{}", "snippet": "\\tableautorefname{$1}", "meta": "hypdvips-cmd", "score": 0.00012704528567339081}, {"caption": "\\equationautorefname", "snippet": "\\equationautorefname", "meta": "hypdvips-cmd", "score": 0.00018777198999871106}, {"caption": "\\equationautorefname{}", "snippet": "\\equationautorefname{$1}", "meta": "hypdvips-cmd", "score": 0.00018777198999871106}, {"caption": "\\chapterautorefname", "snippet": "\\chapterautorefname", "meta": "hypdvips-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\TeX", "snippet": "\\TeX", "meta": "hypdvips-cmd", "score": 0.02873756018238537}, {"caption": "\\TeX{}", "snippet": "\\TeX{$1}", "meta": "hypdvips-cmd", "score": 0.02873756018238537}, {"caption": "\\protect", "snippet": "\\protect", "meta": "hypdvips-cmd", "score": 0.0200686676229443}, {"caption": "\\appendixautorefname", "snippet": "\\appendixautorefname", "meta": "hypdvips-cmd", "score": 7.950698053641679e-05}, {"caption": "\\appendixautorefname{}", "snippet": "\\appendixautorefname{$1}", "meta": "hypdvips-cmd", "score": 7.950698053641679e-05}, {"caption": "\\newlabel{}{}", "snippet": "\\newlabel{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.00029737672328168955}, {"caption": "\\texorpdfstring{}{}", "snippet": "\\texorpdfstring{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.0073781967296121}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "hypdvips-cmd", "score": 0.002140559856649122}, {"caption": "\\alph", "snippet": "\\alph", "meta": "hypdvips-cmd", "score": 0.01034327266194849}, {"caption": "\\alph{}", "snippet": "\\alph{$1}", "meta": "hypdvips-cmd", "score": 0.01034327266194849}, {"caption": "\\pageref{}", "snippet": "\\pageref{$1}", "meta": "hypdvips-cmd", "score": 0.019788865471151957}, {"caption": "\\item", "snippet": "\\item", "meta": "hypdvips-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "hypdvips-cmd", "score": 3.800886892251021}, {"caption": "\\LaTeX", "snippet": "\\LaTeX", "meta": "hypdvips-cmd", "score": 0.2334089308452787}, {"caption": "\\LaTeX{}", "snippet": "\\LaTeX{$1}", "meta": "hypdvips-cmd", "score": 0.2334089308452787}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\itemautorefname", "snippet": "\\itemautorefname", "meta": "hypdvips-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "hypdvips-cmd", "score": 1.2569477427490174}, {"caption": "\\sectionautorefname", "snippet": "\\sectionautorefname", "meta": "hypdvips-cmd", "score": 0.0019832324299155183}, {"caption": "\\sectionautorefname{}", "snippet": "\\sectionautorefname{$1}", "meta": "hypdvips-cmd", "score": 0.0019832324299155183}, {"caption": "\\LaTeXe", "snippet": "\\LaTeXe", "meta": "hypdvips-cmd", "score": 0.007928096378157487}, {"caption": "\\LaTeXe{}", "snippet": "\\LaTeXe{$1}", "meta": "hypdvips-cmd", "score": 0.007928096378157487}, {"caption": "\\footref{}", "snippet": "\\footref{$1}", "meta": "hypdvips-cmd", "score": 0.0003680857021151614}, {"caption": "\\footref", "snippet": "\\footref", "meta": "hypdvips-cmd", "score": 0.0003680857021151614}, {"caption": "\\hypertarget{}{}", "snippet": "\\hypertarget{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.009652820108904094}, {"caption": "\\theoremautorefname", "snippet": "\\theoremautorefname", "meta": "hypdvips-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "hypdvips-cmd", "score": 0.7504160124360846}, {"caption": "\\subparagraphautorefname", "snippet": "\\subparagraphautorefname", "meta": "hypdvips-cmd", "score": 0.0005446476945175932}, {"caption": "\\url{}", "snippet": "\\url{$1}", "meta": "hypdvips-cmd", "score": 0.13586474005868793}, {"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "hypdvips-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "hypdvips-cmd", "score": 0.8973590434087177}, {"caption": "\\href{}{}", "snippet": "\\href{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.27111130260612365}, {"caption": "\\Roman{}", "snippet": "\\Roman{$1}", "meta": "hypdvips-cmd", "score": 0.0038703587462843594}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hypdvips-cmd", "score": 0.00530510025314411}, {"caption": "\\autoref{}", "snippet": "\\autoref{$1}", "meta": "hypdvips-cmd", "score": 0.03741172773691362}, {"caption": "\\nolinkurl{}", "snippet": "\\nolinkurl{$1}", "meta": "hypdvips-cmd", "score": 0.0004995635515943437}, {"caption": "\\end{}", "snippet": "\\end{$1}", "meta": "hypdvips-cmd", "score": 7.847906405228455}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "hypdvips-cmd", "score": 0.0174633138331273}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "hypdvips-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "hypdvips-cmd", "score": 0.006776001543888959}, {"caption": "\\partautorefname", "snippet": "\\partautorefname", "meta": "hypdvips-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\Itemautorefname{}", "snippet": "\\Itemautorefname{$1}", "meta": "hypdvips-cmd", "score": 6.006262128895586e-05}, {"caption": "\\halign{}", "snippet": "\\halign{$1}", "meta": "hypdvips-cmd", "score": 0.00017906650306643613}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.20852115286477566}, {"caption": "\\ref{}", "snippet": "\\ref{$1}", "meta": "hypdvips-cmd", "score": 1.4380093454211778}, {"caption": "\\Alph{}", "snippet": "\\Alph{$1}", "meta": "hypdvips-cmd", "score": 0.002233258780143355}, {"caption": "\\Alph", "snippet": "\\Alph", "meta": "hypdvips-cmd", "score": 0.002233258780143355}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "hypdvips-cmd", "score": 0.047007158741781095}, {"caption": "\\MP", "snippet": "\\MP", "meta": "hypdvips-cmd", "score": 0.00018344383742255004}, {"caption": "\\MP{}", "snippet": "\\MP{$1}", "meta": "hypdvips-cmd", "score": 0.00018344383742255004}, {"caption": "\\paragraphautorefname", "snippet": "\\paragraphautorefname", "meta": "hypdvips-cmd", "score": 0.0005446476945175932}, {"caption": "\\citeN{}", "snippet": "\\citeN{$1}", "meta": "hypdvips-cmd", "score": 0.0018503938529945614}, {"caption": "\\citeN", "snippet": "\\citeN", "meta": "hypdvips-cmd", "score": 0.0018503938529945614}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "hypdvips-cmd", "score": 0.07503475348393239}, {"caption": "\\subsectionautorefname", "snippet": "\\subsectionautorefname", "meta": "hypdvips-cmd", "score": 0.0012546605780895737}, {"caption": "\\subsectionautorefname{}", "snippet": "\\subsectionautorefname{$1}", "meta": "hypdvips-cmd", "score": 0.0012546605780895737}, {"caption": "\\hyperref[]{}", "snippet": "\\hyperref[$1]{$2}", "meta": "hypdvips-cmd", "score": 0.004515152477030062}, {"caption": "\\arabic{}", "snippet": "\\arabic{$1}", "meta": "hypdvips-cmd", "score": 0.02445837629741638}, {"caption": "\\arabic", "snippet": "\\arabic", "meta": "hypdvips-cmd", "score": 0.02445837629741638}, {"caption": "\\newline", "snippet": "\\newline", "meta": "hypdvips-cmd", "score": 0.3311721696201715}, {"caption": "\\hypersetup{}", "snippet": "\\hypersetup{$1}", "meta": "hypdvips-cmd", "score": 0.06967310843464661}, {"caption": "\\subsubsectionautorefname", "snippet": "\\subsubsectionautorefname", "meta": "hypdvips-cmd", "score": 0.0012064581899162352}, {"caption": "\\subsubsectionautorefname{}", "snippet": "\\subsubsectionautorefname{$1}", "meta": "hypdvips-cmd", "score": 0.0012064581899162352}, {"caption": "\\title{}", "snippet": "\\title{$1}", "meta": "hypdvips-cmd", "score": 0.9202908262245683}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hypdvips-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hypdvips-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hypdvips-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hypdvips-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\pdfbookmark[]{}{}", "snippet": "\\pdfbookmark[$1]{$2}{$3}", "meta": "hypdvips-cmd", "score": 0.006492248863367502}, {"caption": "\\bookmarkget{}", "snippet": "\\bookmarkget{$1}", "meta": "hypdvips-cmd", "score": 0.00026847053008917257}, {"caption": "\\bookmarksetup{}", "snippet": "\\bookmarksetup{$1}", "meta": "hypdvips-cmd", "score": 0.001134118016265821}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hypdvips-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "hypdvips-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "hypdvips-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "hypdvips-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "hypdvips-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "hypdvips-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hypdvips-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "hypdvips-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "hypdvips-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "hypdvips-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hypdvips-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "hypdvips-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hypdvips-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "hypdvips-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "hypdvips-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "hypdvips-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "hypdvips-cmd", "score": 0.2864294797053033}], "easyReview": [{"caption": "\\highlight{}", "snippet": "\\highlight{$1}", "meta": "easyReview-cmd", "score": 0.00021546602164732416}, {"caption": "\\highlight", "snippet": "\\highlight", "meta": "easyReview-cmd", "score": 0.00021546602164732416}, {"caption": "\\alert{}", "snippet": "\\alert{$1}", "meta": "easyReview-cmd", "score": 0.02756568949970745}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "easyReview-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "easyReview-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "easyReview-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "easyReview-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "easyReview-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "easyReview-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "easyReview-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "easyReview-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "easyReview-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "easyReview-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "easyReview-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "easyReview-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "easyReview-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "easyReview-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "easyReview-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "easyReview-cmd", "score": 0.004649150613625593}, {"caption": "\\missingfigure[]{}", "snippet": "\\missingfigure[$1]{$2}", "meta": "easyReview-cmd", "score": 0.001558719179721163}, {"caption": "\\missingfigure", "snippet": "\\missingfigure", "meta": "easyReview-cmd", "score": 0.001558719179721163}, {"caption": "\\todototoc", "snippet": "\\todototoc", "meta": "easyReview-cmd", "score": 0.000325977535138643}, {"caption": "\\todo{}", "snippet": "\\todo{$1}", "meta": "easyReview-cmd", "score": 0.04115074278362878}, {"caption": "\\todo[]{}", "snippet": "\\todo[$1]{$2}", "meta": "easyReview-cmd", "score": 0.04115074278362878}, {"caption": "\\todo", "snippet": "\\todo", "meta": "easyReview-cmd", "score": 0.04115074278362878}, {"caption": "\\listoftodos", "snippet": "\\listoftodos", "meta": "easyReview-cmd", "score": 0.0005325975940754609}, {"caption": "\\listoftodos[]", "snippet": "\\listoftodos[$1]", "meta": "easyReview-cmd", "score": 0.0005325975940754609}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "easyReview-cmd", "score": 0.0174633138331273}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "easyReview-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "easyReview-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "easyReview-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "easyReview-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "easyReview-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "easyReview-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "easyReview-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "easyReview-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "easyReview-cmd", "score": 0.028955796305270766}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "easyReview-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "easyReview-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "easyReview-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "easyReview-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "easyReview-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "easyReview-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "easyReview-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "easyReview-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "easyReview-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareRobustCommand{}{}", "snippet": "\\DeclareRobustCommand{$1}{$2}", "meta": "easyReview-cmd", "score": 0.0010373158471650705}, {"caption": "\\DeclareRobustCommand{}[]{}", "snippet": "\\DeclareRobustCommand{$1}[$2]{$3}", "meta": "easyReview-cmd", "score": 0.0010373158471650705}, {"caption": "\\sethlcolor{}", "snippet": "\\sethlcolor{$1}", "meta": "easyReview-cmd", "score": 0.01970230898277056}, {"caption": "\\st", "snippet": "\\st", "meta": "easyReview-cmd", "score": 0.004652662833362787}, {"caption": "\\st{}", "snippet": "\\st{$1}", "meta": "easyReview-cmd", "score": 0.004652662833362787}, {"caption": "\\def", "snippet": "\\def", "meta": "easyReview-cmd", "score": 0.21357759092476175}, {"caption": "\\hl{}", "snippet": "\\hl{$1}", "meta": "easyReview-cmd", "score": 0.03421486301062431}, {"caption": "\\sodef", "snippet": "\\sodef", "meta": "easyReview-cmd", "score": 0.0017045357696831268}, {"caption": "\\csname", "snippet": "\\csname", "meta": "easyReview-cmd", "score": 0.008565354665444157}, {"caption": "\\so", "snippet": "\\so", "meta": "easyReview-cmd", "score": 0.004308800134587786}, {"caption": "\\so{}", "snippet": "\\so{$1}", "meta": "easyReview-cmd", "score": 0.004308800134587786}, {"caption": "\\csname", "snippet": "\\csname", "meta": "easyReview-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "easyReview-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "easyReview-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "easyReview-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "easyReview-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "easyReview-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "easyReview-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "easyReview-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "easyReview-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "easyReview-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "easyReview-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "easyReview-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "easyReview-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "easyReview-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "easyReview-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "easyReview-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "easyReview-cmd", "score": 0.2864294797053033}], "quoting": [{"caption": "\\par", "snippet": "\\par", "meta": "quoting-cmd", "score": 0.413853376001159}, {"caption": "\\empty", "snippet": "\\empty", "meta": "quoting-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "quoting-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "quoting-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "quoting-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "quoting-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "quoting-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "quoting-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "quoting-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "quoting-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "quoting-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "quoting-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "quoting-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "quoting-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "quoting-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "quoting-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "quoting-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "quoting-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "quoting-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "quoting-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "quoting-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "quoting-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "quoting-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "quoting-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "quoting-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "quoting-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "quoting-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "quoting-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "quoting-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "quoting-cmd", "score": 0.021170869458413965}, {"caption": "\\empty", "snippet": "\\empty", "meta": "quoting-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "quoting-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "quoting-cmd", "score": 0.008565354665444157}], "fouriernc": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "fouriernc-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "fouriernc-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "fouriernc-cmd", "score": 0.021170869458413965}], "realboxes": [{"caption": "\\Rotatebox{}{}", "snippet": "\\Rotatebox{$1}{$2}", "meta": "realboxes-cmd", "score": 1.8920528094586312e-05}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "realboxes-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "realboxes-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "realboxes-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "realboxes-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "realboxes-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "realboxes-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "realboxes-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "realboxes-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "realboxes-cmd", "score": 0.028955796305270766}, {"caption": "\\shadowbox{}", "snippet": "\\shadowbox{$1}", "meta": "realboxes-cmd", "score": 0.00107667147399019}, {"caption": "\\doublebox", "snippet": "\\doublebox", "meta": "realboxes-cmd", "score": 0.00015142240898356106}, {"caption": "\\VerbatimEnvironment", "snippet": "\\VerbatimEnvironment", "meta": "realboxes-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\thisfancypage{}{}", "snippet": "\\thisfancypage{$1}{$2}", "meta": "realboxes-cmd", "score": 0.00015142240898356106}, {"caption": "\\TheSbox", "snippet": "\\TheSbox", "meta": "realboxes-cmd", "score": 4.5350034239275855e-05}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "realboxes-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "realboxes-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "realboxes-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "realboxes-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "realboxes-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "realboxes-cmd", "score": 0.0018957469739775527}], "etextools": [{"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "etextools-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "etextools-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "etextools-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "etextools-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "etextools-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "etextools-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "etextools-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "etextools-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "etextools-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "etextools-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "etextools-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "etextools-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "etextools-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "etextools-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "etextools-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "etextools-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "etextools-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "etextools-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "etextools-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "etextools-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "etextools-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "etextools-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "etextools-cmd", "score": 0.008565354665444157}, {"caption": "\\reserveinserts{}", "snippet": "\\reserveinserts{$1}", "meta": "etextools-cmd", "score": 0.0018653410309739879}, {"caption": "\\newtoks", "snippet": "\\newtoks", "meta": "etextools-cmd", "score": 0.00031058155311734754}], "ccaption": [{"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "ccaption-cmd", "score": 1.2569477427490174}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "ccaption-cmd", "score": 1.897791904799601}], "exercise": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "exercise-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "exercise-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "exercise-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "exercise-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "exercise-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "exercise-cmd", "score": 0.0018957469739775527}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "exercise-cmd", "score": 0.00037306820619479756}], "slantsc": [{"caption": "\\scshape", "snippet": "\\scshape", "meta": "slantsc-cmd", "score": 0.05364108855914402}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "slantsc-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "slantsc-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "slantsc-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "slantsc-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "slantsc-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "slantsc-cmd", "score": 0.0018957469739775527}], "glossary-longbooktabs": [{"caption": "\\specialrule{}{}{}", "snippet": "\\specialrule{$1}{$2}{$3}", "meta": "glossary-longbooktabs-cmd", "score": 0.004974385202605165}, {"caption": "\\cmidrule", "snippet": "\\cmidrule", "meta": "glossary-longbooktabs-cmd", "score": 0.01894952272365088}, {"caption": "\\cmidrule{}", "snippet": "\\cmidrule{$1}", "meta": "glossary-longbooktabs-cmd", "score": 0.01894952272365088}, {"caption": "\\bottomrule", "snippet": "\\bottomrule", "meta": "glossary-longbooktabs-cmd", "score": 0.04533364657852219}, {"caption": "\\midrule", "snippet": "\\midrule", "meta": "glossary-longbooktabs-cmd", "score": 0.07098077735912875}, {"caption": "\\addlinespace", "snippet": "\\addlinespace", "meta": "glossary-longbooktabs-cmd", "score": 0.005865460617491447}, {"caption": "\\addlinespace[]", "snippet": "\\addlinespace[$1]", "meta": "glossary-longbooktabs-cmd", "score": 0.005865460617491447}, {"caption": "\\toprule", "snippet": "\\toprule", "meta": "glossary-longbooktabs-cmd", "score": 0.059857788139528495}, {"caption": "\\endhead", "snippet": "\\endhead", "meta": "glossary-longbooktabs-cmd", "score": 0.0023853501147448834}, {"caption": "\\endfoot", "snippet": "\\endfoot", "meta": "glossary-longbooktabs-cmd", "score": 0.00044045261916551967}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "glossary-longbooktabs-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "glossary-longbooktabs-cmd", "score": 0.021170869458413965}, {"caption": "\\nopagebreak", "snippet": "\\nopagebreak", "meta": "glossary-longbooktabs-cmd", "score": 9.952664522415981e-05}, {"caption": "\\endfirsthead", "snippet": "\\endfirsthead", "meta": "glossary-longbooktabs-cmd", "score": 0.0016148498709822416}, {"caption": "\\endlastfoot", "snippet": "\\endlastfoot", "meta": "glossary-longbooktabs-cmd", "score": 0.00044045261916551967}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "glossary-longbooktabs-cmd", "score": 0.3277033727934986}, {"caption": "\\tablename", "snippet": "\\tablename", "meta": "glossary-longbooktabs-cmd", "score": 0.0029238994233674776}, {"caption": "\\pagebreak", "snippet": "\\pagebreak", "meta": "glossary-longbooktabs-cmd", "score": 0.0313525090421608}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "glossary-longbooktabs-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "glossary-longbooktabs-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "glossary-longbooktabs-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "glossary-longbooktabs-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "glossary-longbooktabs-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "glossary-longbooktabs-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "glossary-longbooktabs-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "glossary-longbooktabs-cmd", "score": 0.018615449342361392}], "pgflibraryarrows": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgflibraryarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgflibraryarrows-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgflibraryarrows-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgflibraryarrows-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgflibraryarrows-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgflibraryarrows-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgflibraryarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgflibraryarrows-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgflibraryarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgflibraryarrows-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgflibraryarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgflibraryarrows-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgflibraryarrows-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgflibraryarrows-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgflibraryarrows-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgflibraryarrows-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgflibraryarrows-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgflibraryarrows-cmd", "score": 0.2864294797053033}], "soulpos": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "soulpos-cmd", "score": 0.00037306820619479756}], "gmp": [{"caption": "\\par", "snippet": "\\par", "meta": "gmp-cmd", "score": 0.413853376001159}, {"caption": "\\csname", "snippet": "\\csname", "meta": "gmp-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "gmp-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "gmp-cmd", "score": 0.00037306820619479756}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "gmp-cmd", "score": 0.00021116765384691477}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "gmp-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "gmp-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "gmp-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "gmp-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "gmp-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "gmp-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "gmp-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "gmp-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "gmp-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "gmp-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "gmp-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "gmp-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "gmp-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "gmp-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "gmp-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "gmp-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "gmp-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "gmp-cmd", "score": 0.004719094298848707}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "gmp-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "gmp-cmd", "score": 0.021170869458413965}], "csvsimple": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "csvsimple-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "csvsimple-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "csvsimple-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "csvsimple-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "csvsimple-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "csvsimple-cmd", "score": 0.0018957469739775527}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "csvsimple-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "csvsimple-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "csvsimple-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "csvsimple-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "csvsimple-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "csvsimple-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "csvsimple-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "csvsimple-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "csvsimple-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "csvsimple-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "csvsimple-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "csvsimple-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "csvsimple-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "csvsimple-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "csvsimple-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "csvsimple-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "csvsimple-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "csvsimple-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "csvsimple-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "csvsimple-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "csvsimple-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "csvsimple-cmd", "score": 0.008565354665444157}], "ebgaramond": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "ebgaramond-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ebgaramond-cmd", "score": 0.008565354665444157}], "boldline": [{"caption": "\\hlineB{}", "snippet": "\\hlineB{$1}", "meta": "boldline-cmd", "score": 0.0009735563258863602}, {"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "boldline-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "boldline-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "boldline-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "boldline-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "boldline-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "boldline-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "boldline-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "boldline-cmd", "score": 0.018615449342361392}], "fontaxes": [{"caption": "\\csname", "snippet": "\\csname", "meta": "fontaxes-cmd", "score": 0.008565354665444157}], "pbsi": [{"caption": "\\bsifamily", "snippet": "\\bsifamily", "meta": "pbsi-cmd", "score": 3.140504277052775e-05}], "tikz-qtree-compat": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-qtree-compat-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tikz-qtree-compat-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tikz-qtree-compat-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tikz-qtree-compat-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-qtree-compat-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-qtree-compat-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-qtree-compat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tikz-qtree-compat-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tikz-qtree-compat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-qtree-compat-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tikz-qtree-compat-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tikz-qtree-compat-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tikz-qtree-compat-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tikz-qtree-compat-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tikz-qtree-compat-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tikz-qtree-compat-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tikz-qtree-compat-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tikz-qtree-compat-cmd", "score": 0.2864294797053033}], "ebgaramond-maths": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "ebgaramond-maths-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "ebgaramond-maths-cmd", "score": 0.008565354665444157}], "complexity": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "complexity-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "complexity-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "complexity-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "complexity-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "complexity-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "complexity-cmd", "score": 0.0018957469739775527}], "everysel": [{"caption": "\\selectfont", "snippet": "\\selectfont", "meta": "everysel-cmd", "score": 0.04598628699063736}], "txfontsb": [{"caption": "\\sqrt{}", "snippet": "\\sqrt{$1}", "meta": "txfontsb-cmd", "score": 0.20240160977404634}], "nath": [{"caption": "\\vert", "snippet": "\\vert", "meta": "nath-cmd", "score": 0.05152912629788525}, {"caption": "\\prod", "snippet": "\\prod", "meta": "nath-cmd", "score": 0.02549889375975901}, {"caption": "\\quad", "snippet": "\\quad", "meta": "nath-cmd", "score": 0.15242755832392743}, {"caption": "\\underbrace{}", "snippet": "\\underbrace{$1}", "meta": "nath-cmd", "score": 0.010373780436850907}, {"caption": "\\sum", "snippet": "\\sum", "meta": "nath-cmd", "score": 0.42607994509619934}, {"caption": "\\delimgrowth", "snippet": "\\delimgrowth", "meta": "nath-cmd", "score": 1.8073688234300064e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "nath-cmd", "score": 1.4341091141105058}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "nath-cmd", "score": 0.11280487530505384}, {"caption": "\\underline{}", "snippet": "\\underline{$1}", "meta": "nath-cmd", "score": 0.14748550887002482}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "nath-cmd", "score": 1.897791904799601}, {"caption": "\\qquad", "snippet": "\\qquad", "meta": "nath-cmd", "score": 0.0878145577017131}], "vietnam": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "vietnam-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "vietnam-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "vietnam-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "vietnam-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "vietnam-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "vietnam-cmd", "score": 0.0018957469739775527}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "vietnam-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "vietnam-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "vietnam-cmd", "score": 0.021170869458413965}], "answers": [{"caption": "\\endverbatim", "snippet": "\\endverbatim", "meta": "answers-cmd", "score": 0.0022216421267780076}, {"caption": "\\verbatim", "snippet": "\\verbatim", "meta": "answers-cmd", "score": 0.0072203369120285256}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "answers-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "answers-cmd", "score": 0.021170869458413965}, {"caption": "\\par", "snippet": "\\par", "meta": "answers-cmd", "score": 0.413853376001159}, {"caption": "\\verbatiminput{}", "snippet": "\\verbatiminput{$1}", "meta": "answers-cmd", "score": 0.0024547099784948665}, {"caption": "\\verbatiminput", "snippet": "\\verbatiminput", "meta": "answers-cmd", "score": 0.0024547099784948665}], "attachfile": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "attachfile-cmd", "score": 0.00037306820619479756}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "attachfile-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "attachfile-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "attachfile-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "attachfile-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "attachfile-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "attachfile-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "attachfile-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "attachfile-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "attachfile-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "attachfile-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "attachfile-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "attachfile-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "attachfile-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\nameref{}", "snippet": "\\nameref{$1}", "meta": "attachfile-cmd", "score": 0.009472569279662113}, {"caption": "\\pdfbookmark[]{}{}", "snippet": "\\pdfbookmark[$1]{$2}{$3}", "meta": "attachfile-cmd", "score": 0.006492248863367502}, {"caption": "\\figureautorefname", "snippet": "\\figureautorefname", "meta": "attachfile-cmd", "score": 0.00014582556188448738}, {"caption": "\\figureautorefname{}", "snippet": "\\figureautorefname{$1}", "meta": "attachfile-cmd", "score": 0.00014582556188448738}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "attachfile-cmd", "score": 0.006963729684667191}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\footnoteautorefname", "snippet": "\\footnoteautorefname", "meta": "attachfile-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\roman{}", "snippet": "\\roman{$1}", "meta": "attachfile-cmd", "score": 0.005553384455935491}, {"caption": "\\roman", "snippet": "\\roman", "meta": "attachfile-cmd", "score": 0.005553384455935491}, {"caption": "\\string", "snippet": "\\string", "meta": "attachfile-cmd", "score": 0.001042697111754002}, {"caption": "\\MakeLowercase{}", "snippet": "\\MakeLowercase{$1}", "meta": "attachfile-cmd", "score": 0.017289599800633146}, {"caption": "\\textunderscore", "snippet": "\\textunderscore", "meta": "attachfile-cmd", "score": 0.001509072212764015}, {"caption": "\\do", "snippet": "\\do", "meta": "attachfile-cmd", "score": 0.009278344180101056}, {"caption": "\\begin{}", "snippet": "\\begin{$1}", "meta": "attachfile-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}[]", "snippet": "\\begin{$1}[$2]", "meta": "attachfile-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}{}", "snippet": "\\begin{$1}{$2}", "meta": "attachfile-cmd", "score": 7.849662248028187}, {"caption": "\\FancyVerbLineautorefname", "snippet": "\\FancyVerbLineautorefname", "meta": "attachfile-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\hyperlink{}{}", "snippet": "\\hyperlink{$1}{$2}", "meta": "attachfile-cmd", "score": 0.00978652043902115}, {"caption": "\\tableautorefname", "snippet": "\\tableautorefname", "meta": "attachfile-cmd", "score": 0.00012704528567339081}, {"caption": "\\tableautorefname{}", "snippet": "\\tableautorefname{$1}", "meta": "attachfile-cmd", "score": 0.00012704528567339081}, {"caption": "\\equationautorefname", "snippet": "\\equationautorefname", "meta": "attachfile-cmd", "score": 0.00018777198999871106}, {"caption": "\\equationautorefname{}", "snippet": "\\equationautorefname{$1}", "meta": "attachfile-cmd", "score": 0.00018777198999871106}, {"caption": "\\chapterautorefname", "snippet": "\\chapterautorefname", "meta": "attachfile-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\TeX", "snippet": "\\TeX", "meta": "attachfile-cmd", "score": 0.02873756018238537}, {"caption": "\\TeX{}", "snippet": "\\TeX{$1}", "meta": "attachfile-cmd", "score": 0.02873756018238537}, {"caption": "\\protect", "snippet": "\\protect", "meta": "attachfile-cmd", "score": 0.0200686676229443}, {"caption": "\\appendixautorefname", "snippet": "\\appendixautorefname", "meta": "attachfile-cmd", "score": 7.950698053641679e-05}, {"caption": "\\appendixautorefname{}", "snippet": "\\appendixautorefname{$1}", "meta": "attachfile-cmd", "score": 7.950698053641679e-05}, {"caption": "\\newlabel{}{}", "snippet": "\\newlabel{$1}{$2}", "meta": "attachfile-cmd", "score": 0.00029737672328168955}, {"caption": "\\texorpdfstring{}{}", "snippet": "\\texorpdfstring{$1}{$2}", "meta": "attachfile-cmd", "score": 0.0073781967296121}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "attachfile-cmd", "score": 0.002140559856649122}, {"caption": "\\alph", "snippet": "\\alph", "meta": "attachfile-cmd", "score": 0.01034327266194849}, {"caption": "\\alph{}", "snippet": "\\alph{$1}", "meta": "attachfile-cmd", "score": 0.01034327266194849}, {"caption": "\\pageref{}", "snippet": "\\pageref{$1}", "meta": "attachfile-cmd", "score": 0.019788865471151957}, {"caption": "\\item", "snippet": "\\item", "meta": "attachfile-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "attachfile-cmd", "score": 3.800886892251021}, {"caption": "\\LaTeX", "snippet": "\\LaTeX", "meta": "attachfile-cmd", "score": 0.2334089308452787}, {"caption": "\\LaTeX{}", "snippet": "\\LaTeX{$1}", "meta": "attachfile-cmd", "score": 0.2334089308452787}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\itemautorefname", "snippet": "\\itemautorefname", "meta": "attachfile-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "attachfile-cmd", "score": 1.2569477427490174}, {"caption": "\\sectionautorefname", "snippet": "\\sectionautorefname", "meta": "attachfile-cmd", "score": 0.0019832324299155183}, {"caption": "\\sectionautorefname{}", "snippet": "\\sectionautorefname{$1}", "meta": "attachfile-cmd", "score": 0.0019832324299155183}, {"caption": "\\LaTeXe", "snippet": "\\LaTeXe", "meta": "attachfile-cmd", "score": 0.007928096378157487}, {"caption": "\\LaTeXe{}", "snippet": "\\LaTeXe{$1}", "meta": "attachfile-cmd", "score": 0.007928096378157487}, {"caption": "\\footref{}", "snippet": "\\footref{$1}", "meta": "attachfile-cmd", "score": 0.0003680857021151614}, {"caption": "\\footref", "snippet": "\\footref", "meta": "attachfile-cmd", "score": 0.0003680857021151614}, {"caption": "\\hypertarget{}{}", "snippet": "\\hypertarget{$1}{$2}", "meta": "attachfile-cmd", "score": 0.009652820108904094}, {"caption": "\\theoremautorefname", "snippet": "\\theoremautorefname", "meta": "attachfile-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "attachfile-cmd", "score": 0.7504160124360846}, {"caption": "\\subparagraphautorefname", "snippet": "\\subparagraphautorefname", "meta": "attachfile-cmd", "score": 0.0005446476945175932}, {"caption": "\\url{}", "snippet": "\\url{$1}", "meta": "attachfile-cmd", "score": 0.13586474005868793}, {"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "attachfile-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "attachfile-cmd", "score": 0.8973590434087177}, {"caption": "\\href{}{}", "snippet": "\\href{$1}{$2}", "meta": "attachfile-cmd", "score": 0.27111130260612365}, {"caption": "\\Roman{}", "snippet": "\\Roman{$1}", "meta": "attachfile-cmd", "score": 0.0038703587462843594}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "attachfile-cmd", "score": 0.00530510025314411}, {"caption": "\\autoref{}", "snippet": "\\autoref{$1}", "meta": "attachfile-cmd", "score": 0.03741172773691362}, {"caption": "\\nolinkurl{}", "snippet": "\\nolinkurl{$1}", "meta": "attachfile-cmd", "score": 0.0004995635515943437}, {"caption": "\\end{}", "snippet": "\\end{$1}", "meta": "attachfile-cmd", "score": 7.847906405228455}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "attachfile-cmd", "score": 0.0174633138331273}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "attachfile-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "attachfile-cmd", "score": 0.006776001543888959}, {"caption": "\\partautorefname", "snippet": "\\partautorefname", "meta": "attachfile-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\Itemautorefname{}", "snippet": "\\Itemautorefname{$1}", "meta": "attachfile-cmd", "score": 6.006262128895586e-05}, {"caption": "\\halign{}", "snippet": "\\halign{$1}", "meta": "attachfile-cmd", "score": 0.00017906650306643613}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "attachfile-cmd", "score": 0.20852115286477566}, {"caption": "\\ref{}", "snippet": "\\ref{$1}", "meta": "attachfile-cmd", "score": 1.4380093454211778}, {"caption": "\\Alph{}", "snippet": "\\Alph{$1}", "meta": "attachfile-cmd", "score": 0.002233258780143355}, {"caption": "\\Alph", "snippet": "\\Alph", "meta": "attachfile-cmd", "score": 0.002233258780143355}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "attachfile-cmd", "score": 0.047007158741781095}, {"caption": "\\MP", "snippet": "\\MP", "meta": "attachfile-cmd", "score": 0.00018344383742255004}, {"caption": "\\MP{}", "snippet": "\\MP{$1}", "meta": "attachfile-cmd", "score": 0.00018344383742255004}, {"caption": "\\paragraphautorefname", "snippet": "\\paragraphautorefname", "meta": "attachfile-cmd", "score": 0.0005446476945175932}, {"caption": "\\citeN{}", "snippet": "\\citeN{$1}", "meta": "attachfile-cmd", "score": 0.0018503938529945614}, {"caption": "\\citeN", "snippet": "\\citeN", "meta": "attachfile-cmd", "score": 0.0018503938529945614}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "attachfile-cmd", "score": 0.07503475348393239}, {"caption": "\\subsectionautorefname", "snippet": "\\subsectionautorefname", "meta": "attachfile-cmd", "score": 0.0012546605780895737}, {"caption": "\\subsectionautorefname{}", "snippet": "\\subsectionautorefname{$1}", "meta": "attachfile-cmd", "score": 0.0012546605780895737}, {"caption": "\\hyperref[]{}", "snippet": "\\hyperref[$1]{$2}", "meta": "attachfile-cmd", "score": 0.004515152477030062}, {"caption": "\\arabic{}", "snippet": "\\arabic{$1}", "meta": "attachfile-cmd", "score": 0.02445837629741638}, {"caption": "\\arabic", "snippet": "\\arabic", "meta": "attachfile-cmd", "score": 0.02445837629741638}, {"caption": "\\newline", "snippet": "\\newline", "meta": "attachfile-cmd", "score": 0.3311721696201715}, {"caption": "\\hypersetup{}", "snippet": "\\hypersetup{$1}", "meta": "attachfile-cmd", "score": 0.06967310843464661}, {"caption": "\\subsubsectionautorefname", "snippet": "\\subsubsectionautorefname", "meta": "attachfile-cmd", "score": 0.0012064581899162352}, {"caption": "\\subsubsectionautorefname{}", "snippet": "\\subsubsectionautorefname{$1}", "meta": "attachfile-cmd", "score": 0.0012064581899162352}, {"caption": "\\title{}", "snippet": "\\title{$1}", "meta": "attachfile-cmd", "score": 0.9202908262245683}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "attachfile-cmd", "score": 0.00530510025314411}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "attachfile-cmd", "score": 0.00926923425734719}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "attachfile-cmd", "score": 0.20852115286477566}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "attachfile-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "attachfile-cmd", "score": 0.0008147200475678891}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "attachfile-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "attachfile-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "attachfile-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "attachfile-cmd", "score": 0.2864294797053033}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "attachfile-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "attachfile-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "attachfile-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "attachfile-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "attachfile-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "attachfile-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "attachfile-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "attachfile-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "attachfile-cmd", "score": 0.028955796305270766}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "attachfile-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "attachfile-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "attachfile-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "attachfile-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "attachfile-cmd", "score": 0.002958865219480927}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "attachfile-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "attachfile-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "attachfile-cmd", "score": 0.00530510025314411}], "doc": [{"caption": "\\do", "snippet": "\\do", "meta": "doc-cmd", "score": 0.009278344180101056}, {"caption": "\\verb", "snippet": "\\verb", "meta": "doc-cmd", "score": 0.1323269725886312}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "doc-cmd", "score": 0.7504160124360846}, {"caption": "\\verbatim", "snippet": "\\verbatim", "meta": "doc-cmd", "score": 0.0072203369120285256}], "tkz-fct": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-fct-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-fct-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-fct-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-fct-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-fct-cmd", "score": 0.004719094298848707}, {"caption": "\\reserveinserts{}", "snippet": "\\reserveinserts{$1}", "meta": "tkz-fct-cmd", "score": 0.0018653410309739879}, {"caption": "\\newtoks", "snippet": "\\newtoks", "meta": "tkz-fct-cmd", "score": 0.00031058155311734754}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-fct-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-fct-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tkz-fct-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tkz-fct-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tkz-fct-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tkz-fct-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-fct-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tkz-fct-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-fct-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tkz-fct-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-fct-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-fct-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tkz-fct-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tkz-fct-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-fct-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-fct-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tkz-fct-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tkz-fct-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tkz-fct-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-fct-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tkz-fct-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-fct-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tkz-fct-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tkz-fct-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tkz-fct-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tkz-fct-cmd", "score": 0.2864294797053033}], "notes2bib": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "notes2bib-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "notes2bib-cmd", "score": 0.2864294797053033}], "stackengine": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "stackengine-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "stackengine-cmd", "score": 0.021170869458413965}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "stackengine-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "stackengine-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "stackengine-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "stackengine-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "stackengine-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "stackengine-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "stackengine-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "stackengine-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "stackengine-cmd", "score": 0.028955796305270766}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "stackengine-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "stackengine-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "stackengine-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "stackengine-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "stackengine-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "stackengine-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "stackengine-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "stackengine-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "stackengine-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "stackengine-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "stackengine-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "stackengine-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "stackengine-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "stackengine-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "stackengine-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "stackengine-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "stackengine-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "stackengine-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "stackengine-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "stackengine-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "stackengine-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "stackengine-cmd", "score": 0.008565354665444157}], "cellspace": [{"caption": "\\endtabular", "snippet": "\\endtabular", "meta": "cellspace-cmd", "score": 0.0005078239917067089}, {"caption": "\\multicolumn{}{}{}", "snippet": "\\multicolumn{$1}{$2}{$3}", "meta": "cellspace-cmd", "score": 0.5473606021405326}, {"caption": "\\array{}", "snippet": "\\array{$1}", "meta": "cellspace-cmd", "score": 2.650484574842396e-05}, {"caption": "\\arraybackslash", "snippet": "\\arraybackslash", "meta": "cellspace-cmd", "score": 0.014532521139459619}, {"caption": "\\tabular{}", "snippet": "\\tabular{$1}", "meta": "cellspace-cmd", "score": 0.0005078239917067089}, {"caption": "\\csname", "snippet": "\\csname", "meta": "cellspace-cmd", "score": 0.008565354665444157}, {"caption": "\\newcolumntype{}[]{}", "snippet": "\\newcolumntype{$1}[$2]{$3}", "meta": "cellspace-cmd", "score": 0.018615449342361392}, {"caption": "\\newcolumntype{}{}", "snippet": "\\newcolumntype{$1}{$2}", "meta": "cellspace-cmd", "score": 0.018615449342361392}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "cellspace-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "cellspace-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "cellspace-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "cellspace-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "cellspace-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "cellspace-cmd", "score": 0.0018957469739775527}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "cellspace-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "cellspace-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "cellspace-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "cellspace-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "cellspace-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "cellspace-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "cellspace-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "cellspace-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "cellspace-cmd", "score": 0.028955796305270766}], "zxjatype": [{"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "zxjatype-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "zxjatype-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "zxjatype-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "zxjatype-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "zxjatype-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "zxjatype-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "zxjatype-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "zxjatype-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "zxjatype-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "zxjatype-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "zxjatype-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "zxjatype-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "zxjatype-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "zxjatype-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "zxjatype-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "zxjatype-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "zxjatype-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "zxjatype-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "zxjatype-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "zxjatype-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "zxjatype-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zxjatype-cmd", "score": 0.008565354665444157}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "zxjatype-cmd", "score": 0.00021116765384691477}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "zxjatype-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "zxjatype-cmd", "score": 0.2864294797053033}], "newclude": [{"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "newclude-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "newclude-cmd", "score": 1.4425339817971206}, {"caption": "\\include{}", "snippet": "\\include{$1}", "meta": "newclude-cmd", "score": 0.1547080054979312}], "pgf-umlcd": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-umlcd-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgf-umlcd-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgf-umlcd-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgf-umlcd-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgf-umlcd-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgf-umlcd-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgf-umlcd-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgf-umlcd-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgf-umlcd-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-umlcd-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgf-umlcd-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgf-umlcd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgf-umlcd-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgf-umlcd-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgf-umlcd-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgf-umlcd-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgf-umlcd-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgf-umlcd-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgf-umlcd-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgf-umlcd-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgf-umlcd-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgf-umlcd-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgf-umlcd-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgf-umlcd-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgf-umlcd-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgf-umlcd-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgf-umlcd-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgf-umlcd-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgf-umlcd-cmd", "score": 0.2864294797053033}], "thm-listof": [{"caption": "\\listtheoremname", "snippet": "\\listtheoremname", "meta": "thm-listof-cmd", "score": 1.9443373798666845e-05}, {"caption": "\\thmtformatoptarg", "snippet": "\\thmtformatoptarg", "meta": "thm-listof-cmd", "score": 6.353668036093916e-05}, {"caption": "\\listoftheorems[]", "snippet": "\\listoftheorems[$1]", "meta": "thm-listof-cmd", "score": 1.9443373798666845e-05}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "thm-listof-cmd", "score": 0.00037306820619479756}, {"caption": "\\empty", "snippet": "\\empty", "meta": "thm-listof-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "thm-listof-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "thm-listof-cmd", "score": 0.008565354665444157}, {"caption": "\\proof{}", "snippet": "\\proof{$1}", "meta": "thm-listof-cmd", "score": 0.000701497773639073}, {"caption": "\\proof", "snippet": "\\proof", "meta": "thm-listof-cmd", "score": 0.000701497773639073}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "thm-listof-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "thm-listof-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "thm-listof-cmd", "score": 0.215689795055434}, {"caption": "\\endproof", "snippet": "\\endproof", "meta": "thm-listof-cmd", "score": 0.0006133100544751855}, {"caption": "\\endproof{}", "snippet": "\\endproof{$1}", "meta": "thm-listof-cmd", "score": 0.0006133100544751855}], "thm-autoref": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "thm-autoref-cmd", "score": 0.00037306820619479756}, {"caption": "\\proof{}", "snippet": "\\proof{$1}", "meta": "thm-autoref-cmd", "score": 0.000701497773639073}, {"caption": "\\proof", "snippet": "\\proof", "meta": "thm-autoref-cmd", "score": 0.000701497773639073}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "thm-autoref-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "thm-autoref-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "thm-autoref-cmd", "score": 0.215689795055434}, {"caption": "\\endproof", "snippet": "\\endproof", "meta": "thm-autoref-cmd", "score": 0.0006133100544751855}, {"caption": "\\endproof{}", "snippet": "\\endproof{$1}", "meta": "thm-autoref-cmd", "score": 0.0006133100544751855}], "thm-patch": [{"caption": "\\proof{}", "snippet": "\\proof{$1}", "meta": "thm-patch-cmd", "score": 0.000701497773639073}, {"caption": "\\proof", "snippet": "\\proof", "meta": "thm-patch-cmd", "score": 0.000701497773639073}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "thm-patch-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "thm-patch-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "thm-patch-cmd", "score": 0.215689795055434}, {"caption": "\\endproof", "snippet": "\\endproof", "meta": "thm-patch-cmd", "score": 0.0006133100544751855}, {"caption": "\\endproof{}", "snippet": "\\endproof{$1}", "meta": "thm-patch-cmd", "score": 0.0006133100544751855}], "thm-kv": [{"caption": "\\declaretheoremstyle[]{}", "snippet": "\\declaretheoremstyle[$1]{$2}", "meta": "thm-kv-cmd", "score": 0.0001168034231635369}, {"caption": "\\declaretheorem[]{}", "snippet": "\\declaretheorem[$1]{$2}", "meta": "thm-kv-cmd", "score": 0.0004904790216915127}, {"caption": "\\theoremstyle{}", "snippet": "\\theoremstyle{$1}", "meta": "thm-kv-cmd", "score": 0.02533412165007986}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "thm-kv-cmd", "score": 0.00037306820619479756}, {"caption": "\\proof{}", "snippet": "\\proof{$1}", "meta": "thm-kv-cmd", "score": 0.000701497773639073}, {"caption": "\\proof", "snippet": "\\proof", "meta": "thm-kv-cmd", "score": 0.000701497773639073}, {"caption": "\\newtheorem{}[]{}", "snippet": "\\newtheorem{$1}[$2]{$3}", "meta": "thm-kv-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}", "snippet": "\\newtheorem{$1}{$2}", "meta": "thm-kv-cmd", "score": 0.215689795055434}, {"caption": "\\newtheorem{}{}[]", "snippet": "\\newtheorem{$1}{$2}[$3]", "meta": "thm-kv-cmd", "score": 0.215689795055434}, {"caption": "\\endproof", "snippet": "\\endproof", "meta": "thm-kv-cmd", "score": 0.0006133100544751855}, {"caption": "\\endproof{}", "snippet": "\\endproof{$1}", "meta": "thm-kv-cmd", "score": 0.0006133100544751855}, {"caption": "\\empty", "snippet": "\\empty", "meta": "thm-kv-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "thm-kv-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "thm-kv-cmd", "score": 0.008565354665444157}], "onlyamsmath": [{"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "onlyamsmath-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "onlyamsmath-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "onlyamsmath-cmd", "score": 0.18137737738638837}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "onlyamsmath-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "onlyamsmath-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "onlyamsmath-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "onlyamsmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "onlyamsmath-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "onlyamsmath-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "onlyamsmath-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "onlyamsmath-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "onlyamsmath-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "onlyamsmath-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "onlyamsmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "onlyamsmath-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "onlyamsmath-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "onlyamsmath-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "onlyamsmath-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "onlyamsmath-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "onlyamsmath-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "onlyamsmath-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "onlyamsmath-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "onlyamsmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "onlyamsmath-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "onlyamsmath-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "onlyamsmath-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "onlyamsmath-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "onlyamsmath-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "onlyamsmath-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "onlyamsmath-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "onlyamsmath-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "onlyamsmath-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "onlyamsmath-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "onlyamsmath-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "onlyamsmath-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "onlyamsmath-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "onlyamsmath-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "onlyamsmath-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "onlyamsmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "onlyamsmath-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "onlyamsmath-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "onlyamsmath-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "onlyamsmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "onlyamsmath-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "onlyamsmath-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "onlyamsmath-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "onlyamsmath-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "onlyamsmath-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "onlyamsmath-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "onlyamsmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "onlyamsmath-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "onlyamsmath-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "onlyamsmath-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "onlyamsmath-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "onlyamsmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "onlyamsmath-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "onlyamsmath-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "onlyamsmath-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "onlyamsmath-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "onlyamsmath-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "onlyamsmath-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "onlyamsmath-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "onlyamsmath-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "onlyamsmath-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "onlyamsmath-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "onlyamsmath-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "onlyamsmath-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "onlyamsmath-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "onlyamsmath-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "onlyamsmath-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "onlyamsmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "onlyamsmath-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "onlyamsmath-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "onlyamsmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "onlyamsmath-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "onlyamsmath-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "onlyamsmath-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "onlyamsmath-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "onlyamsmath-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "onlyamsmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "onlyamsmath-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "onlyamsmath-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "onlyamsmath-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "onlyamsmath-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "onlyamsmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "onlyamsmath-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "onlyamsmath-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "onlyamsmath-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "onlyamsmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "onlyamsmath-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "onlyamsmath-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "onlyamsmath-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "onlyamsmath-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "onlyamsmath-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "onlyamsmath-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "onlyamsmath-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "onlyamsmath-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "onlyamsmath-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "onlyamsmath-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "onlyamsmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "onlyamsmath-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "onlyamsmath-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "onlyamsmath-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "onlyamsmath-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "onlyamsmath-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "onlyamsmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "onlyamsmath-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "onlyamsmath-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "onlyamsmath-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "onlyamsmath-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "onlyamsmath-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "onlyamsmath-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "onlyamsmath-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "onlyamsmath-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "onlyamsmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "onlyamsmath-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "onlyamsmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "onlyamsmath-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "onlyamsmath-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "onlyamsmath-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "onlyamsmath-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "onlyamsmath-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "onlyamsmath-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "onlyamsmath-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "onlyamsmath-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "onlyamsmath-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "onlyamsmath-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "onlyamsmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "onlyamsmath-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "onlyamsmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "onlyamsmath-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "onlyamsmath-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "onlyamsmath-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "onlyamsmath-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "onlyamsmath-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "onlyamsmath-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "onlyamsmath-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "onlyamsmath-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "onlyamsmath-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "onlyamsmath-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "onlyamsmath-cmd", "score": 0.0063276692758974925}], "arsclassica": [{"caption": "\\spacedlowsmallcaps{}", "snippet": "\\spacedlowsmallcaps{$1}", "meta": "arsclassica-cmd", "score": 0.002677188251799468}, {"caption": "\\sectionmark", "snippet": "\\sectionmark", "meta": "arsclassica-cmd", "score": 0.005008938879210868}, {"caption": "\\spacedallcaps{}", "snippet": "\\spacedallcaps{$1}", "meta": "arsclassica-cmd", "score": 0.0015281000475958944}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "arsclassica-cmd", "score": 0.3277033727934986}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "arsclassica-cmd", "score": 0.1789117552185788}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.00037306820619479756}, {"caption": "\\specialrule{}{}{}", "snippet": "\\specialrule{$1}{$2}{$3}", "meta": "arsclassica-cmd", "score": 0.004974385202605165}, {"caption": "\\cmidrule", "snippet": "\\cmidrule", "meta": "arsclassica-cmd", "score": 0.01894952272365088}, {"caption": "\\cmidrule{}", "snippet": "\\cmidrule{$1}", "meta": "arsclassica-cmd", "score": 0.01894952272365088}, {"caption": "\\bottomrule", "snippet": "\\bottomrule", "meta": "arsclassica-cmd", "score": 0.04533364657852219}, {"caption": "\\midrule", "snippet": "\\midrule", "meta": "arsclassica-cmd", "score": 0.07098077735912875}, {"caption": "\\addlinespace", "snippet": "\\addlinespace", "meta": "arsclassica-cmd", "score": 0.005865460617491447}, {"caption": "\\addlinespace[]", "snippet": "\\addlinespace[$1]", "meta": "arsclassica-cmd", "score": 0.005865460617491447}, {"caption": "\\toprule", "snippet": "\\toprule", "meta": "arsclassica-cmd", "score": 0.059857788139528495}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "arsclassica-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "arsclassica-cmd", "score": 0.02900783226643065}, {"caption": "\\captionof{}{}", "snippet": "\\captionof{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.018348594199161503}, {"caption": "\\string", "snippet": "\\string", "meta": "arsclassica-cmd", "score": 0.001042697111754002}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "arsclassica-cmd", "score": 0.047007158741781095}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "arsclassica-cmd", "score": 0.00530510025314411}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "arsclassica-cmd", "score": 0.0030745841706804776}, {"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "arsclassica-cmd", "score": 0.422097569591803}, {"caption": "\\csname", "snippet": "\\csname", "meta": "arsclassica-cmd", "score": 0.008565354665444157}, {"caption": "\\hspace{}", "snippet": "\\hspace{$1}", "meta": "arsclassica-cmd", "score": 0.3147206476372336}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "arsclassica-cmd", "score": 1.2569477427490174}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "arsclassica-cmd", "score": 1.897791904799601}, {"caption": "\\ContinuedFloat", "snippet": "\\ContinuedFloat", "meta": "arsclassica-cmd", "score": 5.806935368083486e-05}, {"caption": "\\noindent", "snippet": "\\noindent", "meta": "arsclassica-cmd", "score": 0.42355747798114207}, {"caption": "\\titleclass{}{}[]", "snippet": "\\titleclass{$1}{$2}[$3]", "meta": "arsclassica-cmd", "score": 0.00028979763314974667}, {"caption": "\\titlelabel{}", "snippet": "\\titlelabel{$1}", "meta": "arsclassica-cmd", "score": 6.40387839367932e-06}, {"caption": "\\thetitle", "snippet": "\\thetitle", "meta": "arsclassica-cmd", "score": 0.0015531478302713473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "arsclassica-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "arsclassica-cmd", "score": 0.021170869458413965}, {"caption": "\\titleformat{}{}{}{}{}[]", "snippet": "\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]", "meta": "arsclassica-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}[]{}{}{}{}", "snippet": "\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "arsclassica-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}{}", "snippet": "\\titleformat{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.03475519439740096}, {"caption": "\\titleformat{}{}{}{}{}", "snippet": "\\titleformat{$1}{$2}{$3}{$4}{$5}", "meta": "arsclassica-cmd", "score": 0.03475519439740096}, {"caption": "\\titlespacing{}{}{}{}", "snippet": "\\titlespacing{$1}{$2}{$3}{$4}", "meta": "arsclassica-cmd", "score": 0.023062744385192156}, {"caption": "\\markboth{}{}", "snippet": "\\markboth{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.038323601301945065}, {"caption": "\\markboth{}", "snippet": "\\markboth{$1}", "meta": "arsclassica-cmd", "score": 0.038323601301945065}, {"caption": "\\markright{}", "snippet": "\\markright{$1}", "meta": "arsclassica-cmd", "score": 0.007138622674767024}, {"caption": "\\markright{}{}", "snippet": "\\markright{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.007138622674767024}, {"caption": "\\filleft", "snippet": "\\filleft", "meta": "arsclassica-cmd", "score": 7.959989906732799e-05}, {"caption": "\\filcenter", "snippet": "\\filcenter", "meta": "arsclassica-cmd", "score": 0.0004835660211260246}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "arsclassica-cmd", "score": 0.2253056071787701}, {"caption": "\\cleardoublepage", "snippet": "\\cleardoublepage", "meta": "arsclassica-cmd", "score": 0.044016804142963585}, {"caption": "\\csname", "snippet": "\\csname", "meta": "arsclassica-cmd", "score": 0.008565354665444157}, {"caption": "\\chaptertitlename", "snippet": "\\chaptertitlename", "meta": "arsclassica-cmd", "score": 0.0016985007766926272}, {"caption": "\\newpage", "snippet": "\\newpage", "meta": "arsclassica-cmd", "score": 0.3277033727934986}, {"caption": "\\filright", "snippet": "\\filright", "meta": "arsclassica-cmd", "score": 7.959989906732799e-05}, {"caption": "\\titlerule", "snippet": "\\titlerule", "meta": "arsclassica-cmd", "score": 0.019273712561461216}, {"caption": "\\titlerule[]{}", "snippet": "\\titlerule[$1]{$2}", "meta": "arsclassica-cmd", "score": 0.019273712561461216}, {"caption": "\\DeclareCaptionJustification{}{}", "snippet": "\\DeclareCaptionJustification{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.0001872850414971473}, {"caption": "\\DeclareCaptionLabelSeparator{}{}", "snippet": "\\DeclareCaptionLabelSeparator{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.0003890810058478364}, {"caption": "\\DeclareCaptionFormat{}{}", "snippet": "\\DeclareCaptionFormat{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.0004717618449370015}, {"caption": "\\DeclareCaptionFont{}{}", "snippet": "\\DeclareCaptionFont{$1}{$2}", "meta": "arsclassica-cmd", "score": 5.0133404990680195e-05}, {"caption": "\\DeclareCaptionSubType[]{}", "snippet": "\\DeclareCaptionSubType[$1]{$2}", "meta": "arsclassica-cmd", "score": 0.0001872850414971473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "arsclassica-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "arsclassica-cmd", "score": 0.021170869458413965}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "arsclassica-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "arsclassica-cmd", "score": 0.02900783226643065}, {"caption": "\\string", "snippet": "\\string", "meta": "arsclassica-cmd", "score": 0.001042697111754002}, {"caption": "\\DeclareCaptionType{}[][]", "snippet": "\\DeclareCaptionType{$1}[$2][$3]", "meta": "arsclassica-cmd", "score": 0.00015256647321237863}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "arsclassica-cmd", "score": 0.00530510025314411}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "arsclassica-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "arsclassica-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "arsclassica-cmd", "score": 0.021473212893597875}, {"caption": "\\marginpar{}", "snippet": "\\marginpar{$1}", "meta": "arsclassica-cmd", "score": 0.003400158497921723}, {"caption": "\\marginpar", "snippet": "\\marginpar", "meta": "arsclassica-cmd", "score": 0.003400158497921723}, {"caption": "\\cftsecleader", "snippet": "\\cftsecleader", "meta": "arsclassica-cmd", "score": 0.0011340882025681251}, {"caption": "\\cftsubsecleader", "snippet": "\\cftsubsecleader", "meta": "arsclassica-cmd", "score": 1.0644172549700836e-05}, {"caption": "\\spacedlowsmallcaps{}", "snippet": "\\spacedlowsmallcaps{$1}", "meta": "arsclassica-cmd", "score": 0.002677188251799468}, {"caption": "\\sectionmark", "snippet": "\\sectionmark", "meta": "arsclassica-cmd", "score": 0.005008938879210868}, {"caption": "\\chaptermark", "snippet": "\\chaptermark", "meta": "arsclassica-cmd", "score": 0.005924520024686584}, {"caption": "\\chaptermark{}", "snippet": "\\chaptermark{$1}", "meta": "arsclassica-cmd", "score": 0.005924520024686584}, {"caption": "\\part{}", "snippet": "\\part{$1}", "meta": "arsclassica-cmd", "score": 0.022180129487444723}, {"caption": "\\tocEntry{}", "snippet": "\\tocEntry{$1}", "meta": "arsclassica-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\graffito{}", "snippet": "\\graffito{$1}", "meta": "arsclassica-cmd", "score": 1.1006799670632527e-05}, {"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "arsclassica-cmd", "score": 0.422097569591803}, {"caption": "\\spacedallcaps{}", "snippet": "\\spacedallcaps{$1}", "meta": "arsclassica-cmd", "score": 0.0015281000475958944}, {"caption": "\\cftchapleader", "snippet": "\\cftchapleader", "meta": "arsclassica-cmd", "score": 1.0644172549700836e-05}, {"caption": "\\myVersion", "snippet": "\\myVersion", "meta": "arsclassica-cmd", "score": 0.00018029288638573757}, {"caption": "\\ctparttext{}", "snippet": "\\ctparttext{$1}", "meta": "arsclassica-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\addtokomafont{}{}", "snippet": "\\addtokomafont{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.0008555564394100388}, {"caption": "\\setkomafont{}{}", "snippet": "\\setkomafont{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.012985816912639263}, {"caption": "\\KOMAoptions{}", "snippet": "\\KOMAoptions{$1}", "meta": "arsclassica-cmd", "score": 0.000396664302361659}, {"caption": "\\cite{}", "snippet": "\\cite{$1}", "meta": "arsclassica-cmd", "score": 2.341195220791228}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "arsclassica-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "arsclassica-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "arsclassica-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "arsclassica-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "arsclassica-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "arsclassica-cmd", "score": 0.0018957469739775527}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "arsclassica-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "arsclassica-cmd", "score": 0.021170869458413965}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "arsclassica-cmd", "score": 0.00530510025314411}, {"caption": "\\lsstyle", "snippet": "\\lsstyle", "meta": "arsclassica-cmd", "score": 0.0023367519914345774}, {"caption": "\\space", "snippet": "\\space", "meta": "arsclassica-cmd", "score": 0.023010789853665694}, {"caption": "\\DisableLigatures[]{}", "snippet": "\\DisableLigatures[$1]{$2}", "meta": "arsclassica-cmd", "score": 0.0009805246614299932}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "arsclassica-cmd", "score": 0.00021116765384691477}], "blkarray": [{"caption": "\\small", "snippet": "\\small", "meta": "blkarray-cmd", "score": 0.2447632045426295}, {"caption": "\\small{}", "snippet": "\\small{$1}", "meta": "blkarray-cmd", "score": 0.2447632045426295}], "tkz-tab": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-tab-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-tab-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "tkz-tab-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "tkz-tab-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "tkz-tab-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "tkz-tab-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-tab-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "tkz-tab-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-tab-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "tkz-tab-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-tab-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-tab-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "tkz-tab-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "tkz-tab-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "tkz-tab-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "tkz-tab-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "tkz-tab-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "tkz-tab-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "tkz-tab-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "tkz-tab-cmd", "score": 0.004719094298848707}, {"caption": "\\reserveinserts{}", "snippet": "\\reserveinserts{$1}", "meta": "tkz-tab-cmd", "score": 0.0018653410309739879}, {"caption": "\\newtoks", "snippet": "\\newtoks", "meta": "tkz-tab-cmd", "score": 0.00031058155311734754}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-tab-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "tkz-tab-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tkz-tab-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tkz-tab-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "tkz-tab-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "tkz-tab-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "tkz-tab-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tkz-tab-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "tkz-tab-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tkz-tab-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "tkz-tab-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "tkz-tab-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "tkz-tab-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "tkz-tab-cmd", "score": 0.2864294797053033}], "todo": [{"caption": "\\frak{}", "snippet": "\\frak{$1}", "meta": "todo-cmd", "score": 0.0017966000518546787}, {"caption": "\\checkmark", "snippet": "\\checkmark", "meta": "todo-cmd", "score": 0.025060530944368123}, {"caption": "\\bold", "snippet": "\\bold", "meta": "todo-cmd", "score": 0.0014358547624941567}, {"caption": "\\bold{}", "snippet": "\\bold{$1}", "meta": "todo-cmd", "score": 0.0014358547624941567}, {"caption": "\\Bbb{}", "snippet": "\\Bbb{$1}", "meta": "todo-cmd", "score": 0.0006671850995492977}, {"caption": "\\Bbb", "snippet": "\\Bbb", "meta": "todo-cmd", "score": 0.0006671850995492977}], "lcg": [{"caption": "\\rand", "snippet": "\\rand", "meta": "lcg-cmd", "score": 6.2350576842596716e-06}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "lcg-cmd", "score": 0.00037306820619479756}], "kantlipsum": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "kantlipsum-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "kantlipsum-cmd", "score": 0.2864294797053033}], "chappg": [{"caption": "\\pagenumbering{}", "snippet": "\\pagenumbering{$1}", "meta": "chappg-cmd", "score": 0.06731737633021802}], "chessboard": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "chessboard-cmd", "score": 0.00037306820619479756}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "chessboard-cmd", "score": 0.01590723355124104}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "chessboard-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "chessboard-cmd", "score": 0.009331077109224957}, {"caption": "\\setboardfontencoding{}", "snippet": "\\setboardfontencoding{$1}", "meta": "chessboard-cmd", "score": 0.00014668111964632249}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chessboard-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chessboard-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "chessboard-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "chessboard-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "chessboard-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "chessboard-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "chessboard-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chessboard-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "chessboard-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chessboard-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "chessboard-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chessboard-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chessboard-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chessboard-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "chessboard-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "chessboard-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chessboard-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chessboard-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "chessboard-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "chessboard-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "chessboard-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "chessboard-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "chessboard-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chessboard-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "chessboard-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "chessboard-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chessboard-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "chessboard-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "chessboard-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "chessboard-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "chessboard-cmd", "score": 0.2864294797053033}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "chessboard-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "chessboard-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "chessboard-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chessboard-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chessboard-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "chessboard-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "chessboard-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "chessboard-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "chessboard-cmd", "score": 0.028955796305270766}, {"caption": "\\green", "snippet": "\\green", "meta": "chessboard-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "chessboard-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "chessboard-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "chessboard-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "chessboard-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "chessboard-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "chessboard-cmd", "score": 0.006520475264573554}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "chessboard-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "chessboard-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "chessboard-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "chessboard-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "chessboard-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "chessboard-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chessboard-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chessboard-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chessboard-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chessboard-cmd", "score": 0.008565354665444157}], "xskak": [{"caption": "\\mainline{}", "snippet": "\\mainline{$1}", "meta": "xskak-cmd", "score": 0.0010267678375242572}, {"caption": "\\newchessgame", "snippet": "\\newchessgame", "meta": "xskak-cmd", "score": 0.000880086717877935}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "xskak-cmd", "score": 0.00037306820619479756}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "xskak-cmd", "score": 0.01590723355124104}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "xskak-cmd", "score": 0.009331077109224957}, {"caption": "\\setboardfontencoding{}", "snippet": "\\setboardfontencoding{$1}", "meta": "xskak-cmd", "score": 0.00014668111964632249}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xskak-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xskak-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "xskak-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "xskak-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "xskak-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "xskak-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xskak-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "xskak-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xskak-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "xskak-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xskak-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xskak-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xskak-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "xskak-cmd", "score": 0.004649150613625593}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "xskak-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xskak-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xskak-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "xskak-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "xskak-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "xskak-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xskak-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "xskak-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "xskak-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xskak-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "xskak-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xskak-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xskak-cmd", "score": 0.2864294797053033}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "xskak-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "xskak-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "xskak-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xskak-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xskak-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "xskak-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "xskak-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "xskak-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "xskak-cmd", "score": 0.028955796305270766}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "xskak-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "xskak-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "xskak-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "xskak-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "xskak-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "xskak-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "xskak-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "xskak-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xskak-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "xskak-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "xskak-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "xskak-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "xskak-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "xskak-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "xskak-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "xskak-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "xskak-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "xskak-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "xskak-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "xskak-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "xskak-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "xskak-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xskak-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "xskak-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "xskak-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "xskak-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "xskak-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xskak-cmd", "score": 0.008565354665444157}, {"caption": "\\green", "snippet": "\\green", "meta": "xskak-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "xskak-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "xskak-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "xskak-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "xskak-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "xskak-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "xskak-cmd", "score": 0.006520475264573554}], "pgfheaps": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfheaps-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfheaps-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfheaps-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfheaps-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfheaps-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfheaps-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfheaps-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfheaps-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfheaps-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfheaps-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfheaps-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfheaps-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfheaps-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfheaps-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfheaps-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfheaps-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfheaps-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfheaps-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfheaps-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfheaps-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfheaps-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfheaps-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfheaps-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfheaps-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfheaps-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfheaps-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfheaps-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfheaps-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfheaps-cmd", "score": 0.2864294797053033}], "pgfshade": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfshade-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfshade-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "pgfshade-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "pgfshade-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "pgfshade-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "pgfshade-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfshade-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "pgfshade-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfshade-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "pgfshade-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfshade-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfshade-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "pgfshade-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "pgfshade-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "pgfshade-cmd", "score": 0.004719094298848707}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfshade-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "pgfshade-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pgfshade-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pgfshade-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "pgfshade-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "pgfshade-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "pgfshade-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pgfshade-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "pgfshade-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pgfshade-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "pgfshade-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "pgfshade-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "pgfshade-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "pgfshade-cmd", "score": 0.2864294797053033}], "showframe": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "showframe-cmd", "score": 0.00037306820619479756}, {"caption": "\\empty", "snippet": "\\empty", "meta": "showframe-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "showframe-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "showframe-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "showframe-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "showframe-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "showframe-cmd", "score": 0.008565354665444157}, {"caption": "\\AddToShipoutPictureFG{}", "snippet": "\\AddToShipoutPictureFG{$1}", "meta": "showframe-cmd", "score": 0.000325977535138643}, {"caption": "\\AddToShipoutPictureBG{}", "snippet": "\\AddToShipoutPictureBG{$1}", "meta": "showframe-cmd", "score": 0.0008957666085644653}, {"caption": "\\AtPageUpperLeft{}", "snippet": "\\AtPageUpperLeft{$1}", "meta": "showframe-cmd", "score": 0.0003608141410278152}, {"caption": "\\LenToUnit{}", "snippet": "\\LenToUnit{$1}", "meta": "showframe-cmd", "score": 0.0007216282820556304}, {"caption": "\\AddToShipoutPicture{}", "snippet": "\\AddToShipoutPicture{$1}", "meta": "showframe-cmd", "score": 0.0017658629469099734}], "psvectorian": [{"caption": "\\csname", "snippet": "\\csname", "meta": "psvectorian-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "psvectorian-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "psvectorian-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "psvectorian-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "psvectorian-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "psvectorian-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "psvectorian-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "psvectorian-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "psvectorian-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "psvectorian-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "psvectorian-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "psvectorian-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "psvectorian-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "psvectorian-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "psvectorian-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "psvectorian-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "psvectorian-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "psvectorian-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "psvectorian-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "psvectorian-cmd", "score": 0.004719094298848707}, {"caption": "\\green", "snippet": "\\green", "meta": "psvectorian-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "psvectorian-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "psvectorian-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "psvectorian-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "psvectorian-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "psvectorian-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "psvectorian-cmd", "score": 0.006520475264573554}], "pst-grad": [{"caption": "\\green", "snippet": "\\green", "meta": "pst-grad-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pst-grad-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pst-grad-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pst-grad-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pst-grad-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pst-grad-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pst-grad-cmd", "score": 0.006520475264573554}], "cool": [{"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "cool-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "cool-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "cool-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "cool-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "cool-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "cool-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "cool-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "cool-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "cool-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "cool-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "cool-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "cool-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "cool-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "cool-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "cool-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "cool-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "cool-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "cool-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "cool-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "cool-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "cool-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "cool-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "cool-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "cool-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "cool-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "cool-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "cool-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "cool-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "cool-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "cool-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "cool-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "cool-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "cool-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "cool-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "cool-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "cool-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "cool-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "cool-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "cool-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "cool-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "cool-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "cool-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "cool-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "cool-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "cool-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "cool-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "cool-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "cool-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "cool-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "cool-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "cool-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "cool-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "cool-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "cool-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "cool-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "cool-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "cool-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "cool-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "cool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "cool-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "cool-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "cool-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "cool-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "cool-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "cool-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "cool-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "cool-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "cool-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "cool-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "cool-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "cool-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "cool-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "cool-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "cool-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "cool-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "cool-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "cool-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "cool-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "cool-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "cool-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "cool-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "cool-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "cool-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "cool-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "cool-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "cool-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "cool-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "cool-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "cool-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "cool-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "cool-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "cool-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "cool-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "cool-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "cool-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "cool-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "cool-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "cool-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "cool-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "cool-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "cool-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "cool-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "cool-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "cool-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "cool-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "cool-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "cool-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "cool-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "cool-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "cool-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "cool-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "cool-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "cool-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "cool-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "cool-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "cool-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "cool-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "cool-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "cool-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "cool-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "cool-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "cool-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "cool-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "cool-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "cool-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "cool-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "cool-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "cool-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "cool-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "cool-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "cool-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "cool-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "cool-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "cool-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "cool-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "cool-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "cool-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "cool-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "cool-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "cool-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "cool-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "cool-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "cool-cmd", "score": 0.0004286136584068833}, {"caption": "\\frak{}", "snippet": "\\frak{$1}", "meta": "cool-cmd", "score": 0.0017966000518546787}, {"caption": "\\checkmark", "snippet": "\\checkmark", "meta": "cool-cmd", "score": 0.025060530944368123}, {"caption": "\\bold", "snippet": "\\bold", "meta": "cool-cmd", "score": 0.0014358547624941567}, {"caption": "\\bold{}", "snippet": "\\bold{$1}", "meta": "cool-cmd", "score": 0.0014358547624941567}, {"caption": "\\Bbb{}", "snippet": "\\Bbb{$1}", "meta": "cool-cmd", "score": 0.0006671850995492977}, {"caption": "\\Bbb", "snippet": "\\Bbb", "meta": "cool-cmd", "score": 0.0006671850995492977}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "cool-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "cool-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "cool-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "cool-cmd", "score": 0.008565354665444157}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "cool-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "cool-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "cool-cmd", "score": 0.18137737738638837}, {"caption": "\\forloop{}{}{}{}", "snippet": "\\forloop{$1}{$2}{$3}{$4}", "meta": "cool-cmd", "score": 0.00029867998381154486}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "cool-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "cool-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "cool-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "cool-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "cool-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "cool-cmd", "score": 0.0018957469739775527}, {"caption": "\\do", "snippet": "\\do", "meta": "cool-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "cool-cmd", "score": 0.0063276692758974925}], "xassoccnt": [{"caption": "\\NewTotalDocumentCounter{}", "snippet": "\\NewTotalDocumentCounter{$1}", "meta": "xassoccnt-cmd", "score": 1.5075186740106946e-05}, {"caption": "\\DeclareAssociatedCounters{}{}", "snippet": "\\DeclareAssociatedCounters{$1}{$2}", "meta": "xassoccnt-cmd", "score": 1.5075186740106946e-05}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xassoccnt-cmd", "score": 0.008565354665444157}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "xassoccnt-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "xassoccnt-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "xassoccnt-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "xassoccnt-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "xassoccnt-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "xassoccnt-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "xassoccnt-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "xassoccnt-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "xassoccnt-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "xassoccnt-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "xassoccnt-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "xassoccnt-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "xassoccnt-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "xassoccnt-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "xassoccnt-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "xassoccnt-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xassoccnt-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "xassoccnt-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "xassoccnt-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "xassoccnt-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "xassoccnt-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xassoccnt-cmd", "score": 0.008565354665444157}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xassoccnt-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xassoccnt-cmd", "score": 0.2864294797053033}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "xassoccnt-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xassoccnt-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xassoccnt-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "xassoccnt-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "xassoccnt-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "xassoccnt-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "xassoccnt-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "xassoccnt-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "xassoccnt-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "xassoccnt-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "xassoccnt-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "xassoccnt-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "xassoccnt-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "xassoccnt-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "xassoccnt-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "xassoccnt-cmd", "score": 0.2864294797053033}], "chemscheme": [{"caption": "\\csname", "snippet": "\\csname", "meta": "chemscheme-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "chemscheme-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chemscheme-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemscheme-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chemscheme-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chemscheme-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "chemscheme-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "chemscheme-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "chemscheme-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "chemscheme-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "chemscheme-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chemscheme-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "chemscheme-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemscheme-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "chemscheme-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chemscheme-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chemscheme-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chemscheme-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "chemscheme-cmd", "score": 0.004649150613625593}, {"caption": "\\empty", "snippet": "\\empty", "meta": "chemscheme-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chemscheme-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemscheme-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chemscheme-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "chemscheme-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chemscheme-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chemscheme-cmd", "score": 0.021170869458413965}], "pst-all": [{"caption": "\\green", "snippet": "\\green", "meta": "pst-all-cmd", "score": 0.0016005722621532548}, {"caption": "\\green{}", "snippet": "\\green{$1}", "meta": "pst-all-cmd", "score": 0.0016005722621532548}, {"caption": "\\documentclass[]{}", "snippet": "\\documentclass[$1]{$2}", "meta": "pst-all-cmd", "score": 1.4425339817971206}, {"caption": "\\documentclass{}", "snippet": "\\documentclass{$1}", "meta": "pst-all-cmd", "score": 1.4425339817971206}, {"caption": "\\gray", "snippet": "\\gray", "meta": "pst-all-cmd", "score": 0.0005786730478266738}, {"caption": "\\red{}", "snippet": "\\red{$1}", "meta": "pst-all-cmd", "score": 0.006520475264573554}, {"caption": "\\red", "snippet": "\\red", "meta": "pst-all-cmd", "score": 0.006520475264573554}], "regexpatch": [{"caption": "\\xpatchcmd{}{}{}{}{}", "snippet": "\\xpatchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "regexpatch-cmd", "score": 0.0019344877752147675}, {"caption": "\\xpatchcmd", "snippet": "\\xpatchcmd", "meta": "regexpatch-cmd", "score": 0.0019344877752147675}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "regexpatch-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "regexpatch-cmd", "score": 0.2864294797053033}], "chronosys": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "chronosys-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chronosys-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chronosys-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chronosys-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "chronosys-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "chronosys-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "chronosys-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "chronosys-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "chronosys-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chronosys-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "chronosys-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chronosys-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "chronosys-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chronosys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chronosys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chronosys-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "chronosys-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chronosys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chronosys-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chronosys-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "chronosys-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chronosys-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chronosys-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "chronosys-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "chronosys-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "chronosys-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "chronosys-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "chronosys-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chronosys-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "chronosys-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "chronosys-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chronosys-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "chronosys-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "chronosys-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "chronosys-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "chronosys-cmd", "score": 0.2864294797053033}], "newfloat": [{"caption": "\\DeclareFloatingEnvironment[]{}", "snippet": "\\DeclareFloatingEnvironment[$1]{$2}", "meta": "newfloat-cmd", "score": 2.603029874713569e-05}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "newfloat-cmd", "score": 0.00037306820619479756}], "zref": [{"caption": "\\csname", "snippet": "\\csname", "meta": "zref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "zref-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "zref-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "zref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "zref-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "zref-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "zref-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "zref-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "zref-cmd", "score": 0.002958865219480927}], "bmpsize": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "bmpsize-cmd", "score": 0.00037306820619479756}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bmpsize-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bmpsize-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "bmpsize-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "bmpsize-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "bmpsize-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "bmpsize-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "bmpsize-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bmpsize-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "bmpsize-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bmpsize-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "bmpsize-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "bmpsize-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "bmpsize-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "bmpsize-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "bmpsize-cmd", "score": 0.004649150613625593}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bmpsize-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bmpsize-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "bmpsize-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "bmpsize-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "bmpsize-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bmpsize-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bmpsize-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bmpsize-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bmpsize-cmd", "score": 0.021170869458413965}], "steinmetz": [{"caption": "\\Line", "snippet": "\\Line", "meta": "steinmetz-cmd", "score": 0.0006078790177929149}, {"caption": "\\polygon", "snippet": "\\polygon", "meta": "steinmetz-cmd", "score": 0.0008987552240147395}, {"caption": "\\line", "snippet": "\\line", "meta": "steinmetz-cmd", "score": 0.014519741542622297}, {"caption": "\\polyline", "snippet": "\\polyline", "meta": "steinmetz-cmd", "score": 0.00022468880600368487}, {"caption": "\\vector", "snippet": "\\vector", "meta": "steinmetz-cmd", "score": 0.002970308722584179}], "pageslts": [{"caption": "\\thepage", "snippet": "\\thepage", "meta": "pageslts-cmd", "score": 0.0591555998103519}, {"caption": "\\pagenumbering{}", "snippet": "\\pagenumbering{$1}", "meta": "pageslts-cmd", "score": 0.06731737633021802}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "pageslts-cmd", "score": 0.1789117552185788}, {"caption": "\\global", "snippet": "\\global", "meta": "pageslts-cmd", "score": 0.006609629561859019}, {"caption": "\\makeindex", "snippet": "\\makeindex", "meta": "pageslts-cmd", "score": 0.010304996748556729}, {"caption": "\\index{}", "snippet": "\\index{$1}", "meta": "pageslts-cmd", "score": 0.013774721817648336}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pageslts-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "pageslts-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "pageslts-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pageslts-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pageslts-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pageslts-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pageslts-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pageslts-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pageslts-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pageslts-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pageslts-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pageslts-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pageslts-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pageslts-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pageslts-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pageslts-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pageslts-cmd", "score": 0.008565354665444157}], "chronology": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "chronology-cmd", "score": 0.00037306820619479756}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "chronology-cmd", "score": 0.010241823778997489}, {"caption": "\\setlength{}{}", "snippet": "\\setlength{$1}{$2}", "meta": "chronology-cmd", "score": 0.354445763583904}, {"caption": "\\setlength", "snippet": "\\setlength", "meta": "chronology-cmd", "score": 0.354445763583904}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chronology-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chronology-cmd", "score": 0.021170869458413965}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "chronology-cmd", "score": 0.0030745841706804776}, {"caption": "\\setcounter{}{}", "snippet": "\\setcounter{$1}{$2}", "meta": "chronology-cmd", "score": 0.10068045662118841}, {"caption": "\\addtolength{}{}", "snippet": "\\addtolength{$1}{$2}", "meta": "chronology-cmd", "score": 0.028955796305270766}, {"caption": "\\addtolength", "snippet": "\\addtolength", "meta": "chronology-cmd", "score": 0.028955796305270766}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chronology-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chronology-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chronology-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "chronology-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "chronology-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "chronology-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "chronology-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "chronology-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chronology-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "chronology-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chronology-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "chronology-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chronology-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chronology-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chronology-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "chronology-cmd", "score": 0.004649150613625593}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "chronology-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "chronology-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "chronology-cmd", "score": 0.004719094298848707}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "chronology-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "chronology-cmd", "score": 0.2864294797053033}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "chronology-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "chronology-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "chronology-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "chronology-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "chronology-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "chronology-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "chronology-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "chronology-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "chronology-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "chronology-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "chronology-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "chronology-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "chronology-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "chronology-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "chronology-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "chronology-cmd", "score": 0.2864294797053033}], "spreadtab": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "spreadtab-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "spreadtab-cmd", "score": 0.021170869458413965}], "algpascal": [{"caption": "\\algrenewcommand", "snippet": "\\algrenewcommand", "meta": "algpascal-cmd", "score": 0.0019861803661869416}, {"caption": "\\Statex", "snippet": "\\Statex", "meta": "algpascal-cmd", "score": 0.008622777195102994}, {"caption": "\\BState{}", "snippet": "\\BState{$1}", "meta": "algpascal-cmd", "score": 0.0008685861525307122}, {"caption": "\\BState", "snippet": "\\BState", "meta": "algpascal-cmd", "score": 0.0008685861525307122}, {"caption": "\\algloopdefx{}[][]{}", "snippet": "\\algloopdefx{$1}[$2][$3]{$4}", "meta": "algpascal-cmd", "score": 0.00025315185701145097}, {"caption": "\\algnewcommand", "snippet": "\\algnewcommand", "meta": "algpascal-cmd", "score": 0.0030209395012065327}, {"caption": "\\algnewcommand{}[]{}", "snippet": "\\algnewcommand{$1}[$2]{$3}", "meta": "algpascal-cmd", "score": 0.0030209395012065327}, {"caption": "\\Comment{}", "snippet": "\\Comment{$1}", "meta": "algpascal-cmd", "score": 0.005178604573219454}, {"caption": "\\algblockdefx{}{}[]", "snippet": "\\algblockdefx{$1}{$2}[$3]", "meta": "algpascal-cmd", "score": 0.00025315185701145097}, {"caption": "\\algrenewtext{}{}", "snippet": "\\algrenewtext{$1}{$2}", "meta": "algpascal-cmd", "score": 0.0024415580558825975}, {"caption": "\\algrenewtext{}[]{}", "snippet": "\\algrenewtext{$1}[$2]{$3}", "meta": "algpascal-cmd", "score": 0.0024415580558825975}, {"caption": "\\algblock{}{}", "snippet": "\\algblock{$1}{$2}", "meta": "algpascal-cmd", "score": 0.0007916858220314837}, {"caption": "\\csname", "snippet": "\\csname", "meta": "algpascal-cmd", "score": 0.008565354665444157}, {"caption": "\\algdef{}[]{}{}{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}{$5}{$6}", "meta": "algpascal-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}{}[]{}{}", "snippet": "\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}", "meta": "algpascal-cmd", "score": 0.0003102486920966127}, {"caption": "\\algdef{}[]{}[]{}", "snippet": "\\algdef{$1}[$2]{$3}[$4]{$5}", "meta": "algpascal-cmd", "score": 0.0003102486920966127}, {"caption": "\\algtext{}", "snippet": "\\algtext{$1}", "meta": "algpascal-cmd", "score": 0.0005463612015579842}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "algpascal-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "algpascal-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "algpascal-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "algpascal-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "algpascal-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "algpascal-cmd", "score": 0.0018957469739775527}], "cabin": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "cabin-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "cabin-cmd", "score": 0.008565354665444157}], "erewhon": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "erewhon-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "erewhon-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "erewhon-cmd", "score": 0.021170869458413965}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "erewhon-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "erewhon-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "erewhon-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "erewhon-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "erewhon-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "erewhon-cmd", "score": 0.0018957469739775527}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "erewhon-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "erewhon-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "erewhon-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "erewhon-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "erewhon-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "erewhon-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "erewhon-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "erewhon-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "erewhon-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "erewhon-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "erewhon-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "erewhon-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "erewhon-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "erewhon-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "erewhon-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "erewhon-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "erewhon-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "erewhon-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "erewhon-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "erewhon-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "erewhon-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "erewhon-cmd", "score": 0.008565354665444157}], "tgcursor": [{"caption": "\\empty", "snippet": "\\empty", "meta": "tgcursor-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgcursor-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgcursor-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgcursor-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "tgcursor-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgcursor-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "tgcursor-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "tgcursor-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "tgcursor-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "tgcursor-cmd", "score": 0.021170869458413965}], "ifvtex": [{"caption": "\\csname", "snippet": "\\csname", "meta": "ifvtex-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "ifvtex-cmd", "score": 0.002958865219480927}], "memhfixc": [{"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "memhfixc-cmd", "score": 1.2569477427490174}], "longfigure": [{"caption": "\\newpage", "snippet": "\\newpage", "meta": "longfigure-cmd", "score": 0.3277033727934986}], "lato": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "lato-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "lato-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "lato-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "lato-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "lato-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "lato-cmd", "score": 0.0018957469739775527}, {"caption": "\\scshape", "snippet": "\\scshape", "meta": "lato-cmd", "score": 0.05364108855914402}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "lato-cmd", "score": 0.00037306820619479756}], "authoraftertitle": [{"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "authoraftertitle-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "authoraftertitle-cmd", "score": 0.8973590434087177}, {"caption": "\\title{}", "snippet": "\\title{$1}", "meta": "authoraftertitle-cmd", "score": 0.9202908262245683}], "listofsymbols": [{"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "listofsymbols-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "listofsymbols-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "listofsymbols-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "listofsymbols-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "listofsymbols-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "listofsymbols-cmd", "score": 0.0018957469739775527}], "hvfloat": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hvfloat-cmd", "score": 0.008565354665444157}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "hvfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "hvfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "hvfloat-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "hvfloat-cmd", "score": 0.02900783226643065}, {"caption": "\\captionof{}{}", "snippet": "\\captionof{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.018348594199161503}, {"caption": "\\string", "snippet": "\\string", "meta": "hvfloat-cmd", "score": 0.001042697111754002}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "hvfloat-cmd", "score": 0.047007158741781095}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hvfloat-cmd", "score": 0.00530510025314411}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "hvfloat-cmd", "score": 0.0030745841706804776}, {"caption": "\\chapter{}", "snippet": "\\chapter{$1}", "meta": "hvfloat-cmd", "score": 0.422097569591803}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hvfloat-cmd", "score": 0.008565354665444157}, {"caption": "\\hspace{}", "snippet": "\\hspace{$1}", "meta": "hvfloat-cmd", "score": 0.3147206476372336}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "hvfloat-cmd", "score": 1.2569477427490174}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "hvfloat-cmd", "score": 1.897791904799601}, {"caption": "\\ContinuedFloat", "snippet": "\\ContinuedFloat", "meta": "hvfloat-cmd", "score": 5.806935368083486e-05}, {"caption": "\\noindent", "snippet": "\\noindent", "meta": "hvfloat-cmd", "score": 0.42355747798114207}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hvfloat-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hvfloat-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "hvfloat-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "hvfloat-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "hvfloat-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "hvfloat-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hvfloat-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "hvfloat-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hvfloat-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "hvfloat-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "hvfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "hvfloat-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "hvfloat-cmd", "score": 0.004649150613625593}, {"caption": "\\DeclareCaptionJustification{}{}", "snippet": "\\DeclareCaptionJustification{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.0001872850414971473}, {"caption": "\\DeclareCaptionLabelSeparator{}{}", "snippet": "\\DeclareCaptionLabelSeparator{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.0003890810058478364}, {"caption": "\\DeclareCaptionFormat{}{}", "snippet": "\\DeclareCaptionFormat{$1}{$2}", "meta": "hvfloat-cmd", "score": 0.0004717618449370015}, {"caption": "\\DeclareCaptionFont{}{}", "snippet": "\\DeclareCaptionFont{$1}{$2}", "meta": "hvfloat-cmd", "score": 5.0133404990680195e-05}, {"caption": "\\DeclareCaptionSubType[]{}", "snippet": "\\DeclareCaptionSubType[$1]{$2}", "meta": "hvfloat-cmd", "score": 0.0001872850414971473}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "hvfloat-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "hvfloat-cmd", "score": 0.021170869458413965}, {"caption": "\\captionsetup{}", "snippet": "\\captionsetup{$1}", "meta": "hvfloat-cmd", "score": 0.02900783226643065}, {"caption": "\\captionsetup[]{}", "snippet": "\\captionsetup[$1]{$2}", "meta": "hvfloat-cmd", "score": 0.02900783226643065}, {"caption": "\\string", "snippet": "\\string", "meta": "hvfloat-cmd", "score": 0.001042697111754002}, {"caption": "\\DeclareCaptionType{}[][]", "snippet": "\\DeclareCaptionType{$1}[$2][$3]", "meta": "hvfloat-cmd", "score": 0.00015256647321237863}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hvfloat-cmd", "score": 0.00530510025314411}, {"caption": "\\footnote{}", "snippet": "\\footnote{$1}", "meta": "hvfloat-cmd", "score": 0.2253056071787701}, {"caption": "\\footnotemark[]", "snippet": "\\footnotemark[$1]", "meta": "hvfloat-cmd", "score": 0.021473212893597875}, {"caption": "\\footnotemark", "snippet": "\\footnotemark", "meta": "hvfloat-cmd", "score": 0.021473212893597875}], "thmbox": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "thmbox-cmd", "score": 0.00037306820619479756}], "proba": [{"caption": "\\frak{}", "snippet": "\\frak{$1}", "meta": "proba-cmd", "score": 0.0017966000518546787}, {"caption": "\\checkmark", "snippet": "\\checkmark", "meta": "proba-cmd", "score": 0.025060530944368123}, {"caption": "\\bold", "snippet": "\\bold", "meta": "proba-cmd", "score": 0.0014358547624941567}, {"caption": "\\bold{}", "snippet": "\\bold{$1}", "meta": "proba-cmd", "score": 0.0014358547624941567}, {"caption": "\\Bbb{}", "snippet": "\\Bbb{$1}", "meta": "proba-cmd", "score": 0.0006671850995492977}, {"caption": "\\Bbb", "snippet": "\\Bbb", "meta": "proba-cmd", "score": 0.0006671850995492977}], "datatool": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "datatool-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "datatool-cmd", "score": 0.021170869458413965}, {"caption": "\\longmapsto", "snippet": "\\longmapsto", "meta": "datatool-cmd", "score": 0.0017755897148012264}, {"caption": "\\Check{}", "snippet": "\\Check{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "datatool-cmd", "score": 0.006963729684667191}, {"caption": "\\do", "snippet": "\\do", "meta": "datatool-cmd", "score": 0.009278344180101056}, {"caption": "\\iff", "snippet": "\\iff", "meta": "datatool-cmd", "score": 0.004209937150980285}, {"caption": "\\And", "snippet": "\\And", "meta": "datatool-cmd", "score": 0.0011582952152188854}, {"caption": "\\And{}", "snippet": "\\And{$1}", "meta": "datatool-cmd", "score": 0.0011582952152188854}, {"caption": "\\oint", "snippet": "\\oint", "meta": "datatool-cmd", "score": 0.0028650540724050534}, {"caption": "\\boxed{}", "snippet": "\\boxed{$1}", "meta": "datatool-cmd", "score": 0.0035536135737312827}, {"caption": "\\Ddot{}", "snippet": "\\Ddot{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ignorespacesafterend", "snippet": "\\ignorespacesafterend", "meta": "datatool-cmd", "score": 0.0010893680553454854}, {"caption": "\\nonumber", "snippet": "\\nonumber", "meta": "datatool-cmd", "score": 0.051980653969641216}, {"caption": "\\Breve{}", "snippet": "\\Breve{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\mapsto", "snippet": "\\mapsto", "meta": "datatool-cmd", "score": 0.006473769486518971}, {"caption": "\\over{}", "snippet": "\\over{$1}", "meta": "datatool-cmd", "score": 0.0054372322008878786}, {"caption": "\\over", "snippet": "\\over", "meta": "datatool-cmd", "score": 0.0054372322008878786}, {"caption": "\\bigotimes", "snippet": "\\bigotimes", "meta": "datatool-cmd", "score": 0.000984722260624791}, {"caption": "\\bigoplus", "snippet": "\\bigoplus", "meta": "datatool-cmd", "score": 0.0011508785476242003}, {"caption": "\\theequation", "snippet": "\\theequation", "meta": "datatool-cmd", "score": 0.002995924112493351}, {"caption": "\\bigcap", "snippet": "\\bigcap", "meta": "datatool-cmd", "score": 0.005709261168797874}, {"caption": "\\xrightarrow{}", "snippet": "\\xrightarrow{$1}", "meta": "datatool-cmd", "score": 0.004163642482777231}, {"caption": "\\xrightarrow[]{}", "snippet": "\\xrightarrow[$1]{$2}", "meta": "datatool-cmd", "score": 0.004163642482777231}, {"caption": "\\atop", "snippet": "\\atop", "meta": "datatool-cmd", "score": 0.0006518541515279979}, {"caption": "\\dfrac{}{}", "snippet": "\\dfrac{$1}{$2}", "meta": "datatool-cmd", "score": 0.05397545277891961}, {"caption": "\\pmod", "snippet": "\\pmod", "meta": "datatool-cmd", "score": 0.0011773327219377148}, {"caption": "\\pmod{}", "snippet": "\\pmod{$1}", "meta": "datatool-cmd", "score": 0.0011773327219377148}, {"caption": "\\notag", "snippet": "\\notag", "meta": "datatool-cmd", "score": 0.00322520920930312}, {"caption": "\\int", "snippet": "\\int", "meta": "datatool-cmd", "score": 0.11946660537765894}, {"caption": "\\Vec{}", "snippet": "\\Vec{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\bigvee", "snippet": "\\bigvee", "meta": "datatool-cmd", "score": 0.0011677288242806726}, {"caption": "\\sum", "snippet": "\\sum", "meta": "datatool-cmd", "score": 0.42607994509619934}, {"caption": "\\hookrightarrow", "snippet": "\\hookrightarrow", "meta": "datatool-cmd", "score": 0.0015607282046545064}, {"caption": "\\bigsqcup", "snippet": "\\bigsqcup", "meta": "datatool-cmd", "score": 0.0003468284144579442}, {"caption": "\\hookleftarrow", "snippet": "\\hookleftarrow", "meta": "datatool-cmd", "score": 0.0016498799924012809}, {"caption": "\\Dot{}", "snippet": "\\Dot{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\dots", "snippet": "\\dots", "meta": "datatool-cmd", "score": 0.0847414497955395}, {"caption": "\\genfrac{}{}{}{}{}{}", "snippet": "\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}", "meta": "datatool-cmd", "score": 0.004820143328295316}, {"caption": "\\genfrac", "snippet": "\\genfrac", "meta": "datatool-cmd", "score": 0.004820143328295316}, {"caption": "\\cfrac{}{}", "snippet": "\\cfrac{$1}{$2}", "meta": "datatool-cmd", "score": 0.006765684097139381}, {"caption": "\\Acute{}", "snippet": "\\Acute{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\ldots", "snippet": "\\ldots", "meta": "datatool-cmd", "score": 0.11585556755884258}, {"caption": "\\coprod", "snippet": "\\coprod", "meta": "datatool-cmd", "score": 0.00011383372700282614}, {"caption": "\\impliedby", "snippet": "\\impliedby", "meta": "datatool-cmd", "score": 2.3482915591834053e-05}, {"caption": "\\big", "snippet": "\\big", "meta": "datatool-cmd", "score": 0.05613164277964739}, {"caption": "\\idotsint", "snippet": "\\idotsint", "meta": "datatool-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\Longrightarrow", "snippet": "\\Longrightarrow", "meta": "datatool-cmd", "score": 0.002459139437356601}, {"caption": "\\allowdisplaybreaks", "snippet": "\\allowdisplaybreaks", "meta": "datatool-cmd", "score": 0.005931777024772073}, {"caption": "\\eqref{}", "snippet": "\\eqref{$1}", "meta": "datatool-cmd", "score": 0.06345266254167037}, {"caption": "\\mod", "snippet": "\\mod", "meta": "datatool-cmd", "score": 0.0015181439193121889}, {"caption": "\\mod{}", "snippet": "\\mod{$1}", "meta": "datatool-cmd", "score": 0.0015181439193121889}, {"caption": "\\arraystretch", "snippet": "\\arraystretch", "meta": "datatool-cmd", "score": 0.022224283488673075}, {"caption": "\\arraystretch{}", "snippet": "\\arraystretch{$1}", "meta": "datatool-cmd", "score": 0.022224283488673075}, {"caption": "\\bigg", "snippet": "\\bigg", "meta": "datatool-cmd", "score": 0.04318078602869565}, {"caption": "\\underset{}{}", "snippet": "\\underset{$1}{$2}", "meta": "datatool-cmd", "score": 0.012799893214578391}, {"caption": "\\dotsc", "snippet": "\\dotsc", "meta": "datatool-cmd", "score": 0.0008555101484119994}, {"caption": "\\doteq", "snippet": "\\doteq", "meta": "datatool-cmd", "score": 3.164631070474435e-05}, {"caption": "\\leftroot{}", "snippet": "\\leftroot{$1}", "meta": "datatool-cmd", "score": 6.625561928497235e-05}, {"caption": "\\substack{}", "snippet": "\\substack{$1}", "meta": "datatool-cmd", "score": 0.0037482529712850755}, {"caption": "\\Hat{}", "snippet": "\\Hat{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\frac{}{}", "snippet": "\\frac{$1}{$2}", "meta": "datatool-cmd", "score": 1.4341091141105058}, {"caption": "\\mspace{}", "snippet": "\\mspace{$1}", "meta": "datatool-cmd", "score": 3.423236656565836e-05}, {"caption": "\\Bar{}", "snippet": "\\Bar{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\Grave{}", "snippet": "\\Grave{$1}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\implies", "snippet": "\\implies", "meta": "datatool-cmd", "score": 0.021828316911576096}, {"caption": "\\tbinom", "snippet": "\\tbinom", "meta": "datatool-cmd", "score": 1.3908704929884828e-05}, {"caption": "\\dotsi", "snippet": "\\dotsi", "meta": "datatool-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\bigwedge", "snippet": "\\bigwedge", "meta": "datatool-cmd", "score": 0.000347742918592393}, {"caption": "\\sideset{}{}", "snippet": "\\sideset{$1}{$2}", "meta": "datatool-cmd", "score": 5.563481971953931e-05}, {"caption": "\\smash{}", "snippet": "\\smash{$1}", "meta": "datatool-cmd", "score": 0.008197171096663127}, {"caption": "\\smash[]{}", "snippet": "\\smash[$1]{$2}", "meta": "datatool-cmd", "score": 0.008197171096663127}, {"caption": "\\colon", "snippet": "\\colon", "meta": "datatool-cmd", "score": 0.005300291684408929}, {"caption": "\\intertext{}", "snippet": "\\intertext{$1}", "meta": "datatool-cmd", "score": 0.0016148076375871775}, {"caption": "\\Longleftarrow", "snippet": "\\Longleftarrow", "meta": "datatool-cmd", "score": 8.477207854183949e-05}, {"caption": "\\prod", "snippet": "\\prod", "meta": "datatool-cmd", "score": 0.02549889375975901}, {"caption": "\\AmS", "snippet": "\\AmS", "meta": "datatool-cmd", "score": 0.00047859486202980376}, {"caption": "\\overline{}", "snippet": "\\overline{$1}", "meta": "datatool-cmd", "score": 0.11280487530505384}, {"caption": "\\tfrac{}{}", "snippet": "\\tfrac{$1}{$2}", "meta": "datatool-cmd", "score": 0.0005923542426657187}, {"caption": "\\uproot{}", "snippet": "\\uproot{$1}", "meta": "datatool-cmd", "score": 6.625561928497235e-05}, {"caption": "\\bmod", "snippet": "\\bmod", "meta": "datatool-cmd", "score": 0.002022594681005002}, {"caption": "\\bmod{}", "snippet": "\\bmod{$1}", "meta": "datatool-cmd", "score": 0.002022594681005002}, {"caption": "\\pod{}", "snippet": "\\pod{$1}", "meta": "datatool-cmd", "score": 2.7817409859769657e-05}, {"caption": "\\label{}", "snippet": "\\label{$1}", "meta": "datatool-cmd", "score": 1.897791904799601}, {"caption": "\\longrightarrow", "snippet": "\\longrightarrow", "meta": "datatool-cmd", "score": 0.013399422292458848}, {"caption": "\\xleftarrow[]{}", "snippet": "\\xleftarrow[$1]{$2}", "meta": "datatool-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\xleftarrow{}", "snippet": "\\xleftarrow{$1}", "meta": "datatool-cmd", "score": 3.5779964196240445e-05}, {"caption": "\\mathaccentV", "snippet": "\\mathaccentV", "meta": "datatool-cmd", "score": 6.216218551413489e-05}, {"caption": "\\hdotsfor{}", "snippet": "\\hdotsfor{$1}", "meta": "datatool-cmd", "score": 0.00024247684499275043}, {"caption": "\\hdotsfor[]{}", "snippet": "\\hdotsfor[$1]{$2}", "meta": "datatool-cmd", "score": 0.00024247684499275043}, {"caption": "\\Bigg", "snippet": "\\Bigg", "meta": "datatool-cmd", "score": 0.015507614799858266}, {"caption": "\\Bigg[]", "snippet": "\\Bigg[$1]", "meta": "datatool-cmd", "score": 0.015507614799858266}, {"caption": "\\overset{}{}", "snippet": "\\overset{$1}{$2}", "meta": "datatool-cmd", "score": 0.007611544955294224}, {"caption": "\\Big", "snippet": "\\Big", "meta": "datatool-cmd", "score": 0.050370758781422345}, {"caption": "\\longleftrightarrow", "snippet": "\\longleftrightarrow", "meta": "datatool-cmd", "score": 0.0002851769278703356}, {"caption": "\\Longleftrightarrow", "snippet": "\\Longleftrightarrow", "meta": "datatool-cmd", "score": 0.0004896780659212191}, {"caption": "\\Longleftrightarrow{}", "snippet": "\\Longleftrightarrow{$1}", "meta": "datatool-cmd", "score": 0.0004896780659212191}, {"caption": "\\binom{}{}", "snippet": "\\binom{$1}{$2}", "meta": "datatool-cmd", "score": 0.013010882180364367}, {"caption": "\\longleftarrow", "snippet": "\\longleftarrow", "meta": "datatool-cmd", "score": 0.0011096532692473691}, {"caption": "\\dbinom{}{}", "snippet": "\\dbinom{$1}{$2}", "meta": "datatool-cmd", "score": 0.006800272303210672}, {"caption": "\\Tilde{}", "snippet": "\\Tilde{$1}", "meta": "datatool-cmd", "score": 7.874446783586035e-05}, {"caption": "\\bigcup", "snippet": "\\bigcup", "meta": "datatool-cmd", "score": 0.0058847868741168765}, {"caption": "\\sinh", "snippet": "\\sinh", "meta": "datatool-cmd", "score": 0.0006435164702005918}, {"caption": "\\sinh{}", "snippet": "\\sinh{$1}", "meta": "datatool-cmd", "score": 0.0006435164702005918}, {"caption": "\\operatorname{}", "snippet": "\\operatorname{$1}", "meta": "datatool-cmd", "score": 0.02181954887028883}, {"caption": "\\max", "snippet": "\\max", "meta": "datatool-cmd", "score": 0.04116833357968482}, {"caption": "\\liminf", "snippet": "\\liminf", "meta": "datatool-cmd", "score": 0.0015513861600956144}, {"caption": "\\liminf{}", "snippet": "\\liminf{$1}", "meta": "datatool-cmd", "score": 0.0015513861600956144}, {"caption": "\\operatornamewithlimits{}", "snippet": "\\operatornamewithlimits{$1}", "meta": "datatool-cmd", "score": 0.0022415507993352067}, {"caption": "\\exp", "snippet": "\\exp", "meta": "datatool-cmd", "score": 0.02404262443651467}, {"caption": "\\exp{}", "snippet": "\\exp{$1}", "meta": "datatool-cmd", "score": 0.02404262443651467}, {"caption": "\\lim", "snippet": "\\lim", "meta": "datatool-cmd", "score": 0.05285123457928509}, {"caption": "\\sin", "snippet": "\\sin", "meta": "datatool-cmd", "score": 0.040463088537699636}, {"caption": "\\sin{}", "snippet": "\\sin{$1}", "meta": "datatool-cmd", "score": 0.040463088537699636}, {"caption": "\\arg", "snippet": "\\arg", "meta": "datatool-cmd", "score": 0.007190995792600074}, {"caption": "\\cos", "snippet": "\\cos", "meta": "datatool-cmd", "score": 0.050370402546134785}, {"caption": "\\cos{}", "snippet": "\\cos{$1}", "meta": "datatool-cmd", "score": 0.050370402546134785}, {"caption": "\\varliminf", "snippet": "\\varliminf", "meta": "datatool-cmd", "score": 6.204977642542802e-05}, {"caption": "\\hom", "snippet": "\\hom", "meta": "datatool-cmd", "score": 8.180643329881783e-05}, {"caption": "\\tan", "snippet": "\\tan", "meta": "datatool-cmd", "score": 0.006176447465423192}, {"caption": "\\det", "snippet": "\\det", "meta": "datatool-cmd", "score": 0.005640718203101287}, {"caption": "\\ln", "snippet": "\\ln", "meta": "datatool-cmd", "score": 0.025366949660913504}, {"caption": "\\ln{}", "snippet": "\\ln{$1}", "meta": "datatool-cmd", "score": 0.025366949660913504}, {"caption": "\\cosh", "snippet": "\\cosh", "meta": "datatool-cmd", "score": 0.0008896391580266903}, {"caption": "\\cosh{}", "snippet": "\\cosh{$1}", "meta": "datatool-cmd", "score": 0.0008896391580266903}, {"caption": "\\gcd", "snippet": "\\gcd", "meta": "datatool-cmd", "score": 0.002254008371792865}, {"caption": "\\limsup", "snippet": "\\limsup", "meta": "datatool-cmd", "score": 0.002354950225950599}, {"caption": "\\limsup{}", "snippet": "\\limsup{$1}", "meta": "datatool-cmd", "score": 0.002354950225950599}, {"caption": "\\inf", "snippet": "\\inf", "meta": "datatool-cmd", "score": 0.00340470256994063}, {"caption": "\\arccos", "snippet": "\\arccos", "meta": "datatool-cmd", "score": 0.001781687642431819}, {"caption": "\\arccos{}", "snippet": "\\arccos{$1}", "meta": "datatool-cmd", "score": 0.001781687642431819}, {"caption": "\\ker", "snippet": "\\ker", "meta": "datatool-cmd", "score": 0.002475379242338094}, {"caption": "\\cot", "snippet": "\\cot", "meta": "datatool-cmd", "score": 0.0003640644365701238}, {"caption": "\\cot{}", "snippet": "\\cot{$1}", "meta": "datatool-cmd", "score": 0.0003640644365701238}, {"caption": "\\coth{}", "snippet": "\\coth{$1}", "meta": "datatool-cmd", "score": 0.00025939638266884963}, {"caption": "\\coth", "snippet": "\\coth", "meta": "datatool-cmd", "score": 0.00025939638266884963}, {"caption": "\\varlimsup", "snippet": "\\varlimsup", "meta": "datatool-cmd", "score": 6.204977642542802e-05}, {"caption": "\\log", "snippet": "\\log", "meta": "datatool-cmd", "score": 0.048131780413380156}, {"caption": "\\varinjlim", "snippet": "\\varinjlim", "meta": "datatool-cmd", "score": 0.000361814283649031}, {"caption": "\\deg", "snippet": "\\deg", "meta": "datatool-cmd", "score": 0.005542465148816408}, {"caption": "\\arctan", "snippet": "\\arctan", "meta": "datatool-cmd", "score": 0.0011971697553682045}, {"caption": "\\dim", "snippet": "\\dim", "meta": "datatool-cmd", "score": 0.0038210003967178293}, {"caption": "\\min", "snippet": "\\min", "meta": "datatool-cmd", "score": 0.03051120054363316}, {"caption": "\\Pr", "snippet": "\\Pr", "meta": "datatool-cmd", "score": 0.010227440663206161}, {"caption": "\\Pr[]", "snippet": "\\Pr[$1]", "meta": "datatool-cmd", "score": 0.010227440663206161}, {"caption": "\\tanh", "snippet": "\\tanh", "meta": "datatool-cmd", "score": 0.0021229156376192525}, {"caption": "\\tanh{}", "snippet": "\\tanh{$1}", "meta": "datatool-cmd", "score": 0.0021229156376192525}, {"caption": "\\arcsin", "snippet": "\\arcsin", "meta": "datatool-cmd", "score": 0.0007754886988089101}, {"caption": "\\arcsin{}", "snippet": "\\arcsin{$1}", "meta": "datatool-cmd", "score": 0.0007754886988089101}, {"caption": "\\DeclareMathOperator{}{}", "snippet": "\\DeclareMathOperator{$1}{$2}", "meta": "datatool-cmd", "score": 0.029440493885398676}, {"caption": "\\csc", "snippet": "\\csc", "meta": "datatool-cmd", "score": 0.00013963711107573638}, {"caption": "\\sup", "snippet": "\\sup", "meta": "datatool-cmd", "score": 0.009355514755312534}, {"caption": "\\sec", "snippet": "\\sec", "meta": "datatool-cmd", "score": 0.0005912636157903734}, {"caption": "\\varprojlim", "snippet": "\\varprojlim", "meta": "datatool-cmd", "score": 0.0004286136584068833}, {"caption": "\\stepcounter{}", "snippet": "\\stepcounter{$1}", "meta": "datatool-cmd", "score": 0.0030745841706804776}, {"caption": "\\addtocounter{}{}", "snippet": "\\addtocounter{$1}{$2}", "meta": "datatool-cmd", "score": 0.010241823778997489}, {"caption": "\\text{}", "snippet": "\\text{$1}", "meta": "datatool-cmd", "score": 0.3608680734736821}, {"caption": "\\csname", "snippet": "\\csname", "meta": "datatool-cmd", "score": 0.008565354665444157}, {"caption": "\\pmb{}", "snippet": "\\pmb{$1}", "meta": "datatool-cmd", "score": 0.019171182556792562}, {"caption": "\\boldsymbol{}", "snippet": "\\boldsymbol{$1}", "meta": "datatool-cmd", "score": 0.18137737738638837}, {"caption": "\\boldsymbol", "snippet": "\\boldsymbol", "meta": "datatool-cmd", "score": 0.18137737738638837}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "datatool-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "datatool-cmd", "score": 0.021170869458413965}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "datatool-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "datatool-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "datatool-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "datatool-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "datatool-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "datatool-cmd", "score": 0.0018957469739775527}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "datatool-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "datatool-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "datatool-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "datatool-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "datatool-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "datatool-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "datatool-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "datatool-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "datatool-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "datatool-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "datatool-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "datatool-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "datatool-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "datatool-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "datatool-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "datatool-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "datatool-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "datatool-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "datatool-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "datatool-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "datatool-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "datatool-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "datatool-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "datatool-cmd", "score": 0.0063276692758974925}], "fmtcount": [{"caption": "\\csname", "snippet": "\\csname", "meta": "fmtcount-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "fmtcount-cmd", "score": 0.008565354665444157}, {"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "fmtcount-cmd", "score": 0.00037306820619479756}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "fmtcount-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "fmtcount-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "fmtcount-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "fmtcount-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "fmtcount-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "fmtcount-cmd", "score": 0.0018957469739775527}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "fmtcount-cmd", "score": 0.00021116765384691477}, {"caption": "\\robustify{}", "snippet": "\\robustify{$1}", "meta": "fmtcount-cmd", "score": 0.002671974990314091}, {"caption": "\\setbool{}{}", "snippet": "\\setbool{$1}{$2}", "meta": "fmtcount-cmd", "score": 0.00023171033119130004}, {"caption": "\\ifdefempty{}{}{}", "snippet": "\\ifdefempty{$1}{$2}{$3}", "meta": "fmtcount-cmd", "score": 7.482069221111606e-05}, {"caption": "\\apptocmd{}{}{}{}", "snippet": "\\apptocmd{$1}{$2}{$3}{$4}", "meta": "fmtcount-cmd", "score": 0.00035805058319299113}, {"caption": "\\ifstrequal{}{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}{$4}", "meta": "fmtcount-cmd", "score": 0.00041307691354437894}, {"caption": "\\ifstrequal{}{}{}", "snippet": "\\ifstrequal{$1}{$2}{$3}", "meta": "fmtcount-cmd", "score": 0.00041307691354437894}, {"caption": "\\string", "snippet": "\\string", "meta": "fmtcount-cmd", "score": 0.001042697111754002}, {"caption": "\\csedef{}{}", "snippet": "\\csedef{$1}{$2}", "meta": "fmtcount-cmd", "score": 0.00014933999190577243}, {"caption": "\\do", "snippet": "\\do", "meta": "fmtcount-cmd", "score": 0.009278344180101056}, {"caption": "\\newrobustcmd{}[]{}", "snippet": "\\newrobustcmd{$1}[$2]{$3}", "meta": "fmtcount-cmd", "score": 0.0006607703576475988}, {"caption": "\\ifdefstring{}{}{}{}", "snippet": "\\ifdefstring{$1}{$2}{$3}{$4}", "meta": "fmtcount-cmd", "score": 0.0006796212875843042}, {"caption": "\\ifbool{}{}{}", "snippet": "\\ifbool{$1}{$2}{$3}", "meta": "fmtcount-cmd", "score": 7.723677706376668e-05}, {"caption": "\\patchcmd{}{}{}{}{}", "snippet": "\\patchcmd{$1}{$2}{$3}{$4}{$5}", "meta": "fmtcount-cmd", "score": 0.002560998917940627}, {"caption": "\\patchcmd", "snippet": "\\patchcmd", "meta": "fmtcount-cmd", "score": 0.002560998917940627}, {"caption": "\\preto{}{}", "snippet": "\\preto{$1}{$2}", "meta": "fmtcount-cmd", "score": 8.860754525300578e-05}, {"caption": "\\ifnumcomp{}{}{}{}{}", "snippet": "\\ifnumcomp{$1}{$2}{$3}{$4}{$5}", "meta": "fmtcount-cmd", "score": 0.00029867998381154486}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "fmtcount-cmd", "score": 0.00530510025314411}, {"caption": "\\newbool{}", "snippet": "\\newbool{$1}", "meta": "fmtcount-cmd", "score": 7.723677706376668e-05}, {"caption": "\\AtBeginEnvironment{}{}", "snippet": "\\AtBeginEnvironment{$1}{$2}", "meta": "fmtcount-cmd", "score": 4.002553629215439e-05}, {"caption": "\\pretocmd{}{}{}{}", "snippet": "\\pretocmd{$1}{$2}{$3}{$4}", "meta": "fmtcount-cmd", "score": 0.00028992557275763024}, {"caption": "\\ifundef{}{}{}", "snippet": "\\ifundef{$1}{$2}{$3}", "meta": "fmtcount-cmd", "score": 0.00014933999190577243}, {"caption": "\\csname", "snippet": "\\csname", "meta": "fmtcount-cmd", "score": 0.008565354665444157}, {"caption": "\\do", "snippet": "\\do", "meta": "fmtcount-cmd", "score": 0.009278344180101056}, {"caption": "\\frenchspacing", "snippet": "\\frenchspacing", "meta": "fmtcount-cmd", "score": 0.0063276692758974925}], "aurl": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "aurl-cmd", "score": 0.00037306820619479756}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\AtBeginShipout{}", "snippet": "\\AtBeginShipout{$1}", "meta": "aurl-cmd", "score": 0.00047530324346933345}, {"caption": "\\AtBeginShipoutNext{}", "snippet": "\\AtBeginShipoutNext{$1}", "meta": "aurl-cmd", "score": 0.0005277905480209891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\UrlBreaks{}", "snippet": "\\UrlBreaks{$1}", "meta": "aurl-cmd", "score": 0.001030592515645366}, {"caption": "\\UrlBreaks", "snippet": "\\UrlBreaks", "meta": "aurl-cmd", "score": 0.001030592515645366}, {"caption": "\\Url", "snippet": "\\Url", "meta": "aurl-cmd", "score": 0.0002854206807593436}, {"caption": "\\UrlOrds{}", "snippet": "\\UrlOrds{$1}", "meta": "aurl-cmd", "score": 0.0006882563723629154}, {"caption": "\\UrlOrds", "snippet": "\\UrlOrds", "meta": "aurl-cmd", "score": 0.0006882563723629154}, {"caption": "\\urlstyle{}", "snippet": "\\urlstyle{$1}", "meta": "aurl-cmd", "score": 0.010515056688180681}, {"caption": "\\urldef{}", "snippet": "\\urldef{$1}", "meta": "aurl-cmd", "score": 0.008041789461944983}, {"caption": "\\UrlBigBreaks{}", "snippet": "\\UrlBigBreaks{$1}", "meta": "aurl-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlFont{}", "snippet": "\\UrlFont{$1}", "meta": "aurl-cmd", "score": 0.0032990580087398644}, {"caption": "\\UrlSpecials{}", "snippet": "\\UrlSpecials{$1}", "meta": "aurl-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\UrlNoBreaks", "snippet": "\\UrlNoBreaks", "meta": "aurl-cmd", "score": 3.7048287721105874e-05}, {"caption": "\\nameref{}", "snippet": "\\nameref{$1}", "meta": "aurl-cmd", "score": 0.009472569279662113}, {"caption": "\\pdfbookmark[]{}{}", "snippet": "\\pdfbookmark[$1]{$2}{$3}", "meta": "aurl-cmd", "score": 0.006492248863367502}, {"caption": "\\figureautorefname", "snippet": "\\figureautorefname", "meta": "aurl-cmd", "score": 0.00014582556188448738}, {"caption": "\\figureautorefname{}", "snippet": "\\figureautorefname{$1}", "meta": "aurl-cmd", "score": 0.00014582556188448738}, {"caption": "\\numberwithin{}{}", "snippet": "\\numberwithin{$1}{$2}", "meta": "aurl-cmd", "score": 0.006963729684667191}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\footnoteautorefname", "snippet": "\\footnoteautorefname", "meta": "aurl-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\roman{}", "snippet": "\\roman{$1}", "meta": "aurl-cmd", "score": 0.005553384455935491}, {"caption": "\\roman", "snippet": "\\roman", "meta": "aurl-cmd", "score": 0.005553384455935491}, {"caption": "\\string", "snippet": "\\string", "meta": "aurl-cmd", "score": 0.001042697111754002}, {"caption": "\\MakeLowercase{}", "snippet": "\\MakeLowercase{$1}", "meta": "aurl-cmd", "score": 0.017289599800633146}, {"caption": "\\textunderscore", "snippet": "\\textunderscore", "meta": "aurl-cmd", "score": 0.001509072212764015}, {"caption": "\\do", "snippet": "\\do", "meta": "aurl-cmd", "score": 0.009278344180101056}, {"caption": "\\begin{}", "snippet": "\\begin{$1}", "meta": "aurl-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}[]", "snippet": "\\begin{$1}[$2]", "meta": "aurl-cmd", "score": 7.849662248028187}, {"caption": "\\begin{}{}", "snippet": "\\begin{$1}{$2}", "meta": "aurl-cmd", "score": 7.849662248028187}, {"caption": "\\FancyVerbLineautorefname", "snippet": "\\FancyVerbLineautorefname", "meta": "aurl-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\hyperlink{}{}", "snippet": "\\hyperlink{$1}{$2}", "meta": "aurl-cmd", "score": 0.00978652043902115}, {"caption": "\\tableautorefname", "snippet": "\\tableautorefname", "meta": "aurl-cmd", "score": 0.00012704528567339081}, {"caption": "\\tableautorefname{}", "snippet": "\\tableautorefname{$1}", "meta": "aurl-cmd", "score": 0.00012704528567339081}, {"caption": "\\equationautorefname", "snippet": "\\equationautorefname", "meta": "aurl-cmd", "score": 0.00018777198999871106}, {"caption": "\\equationautorefname{}", "snippet": "\\equationautorefname{$1}", "meta": "aurl-cmd", "score": 0.00018777198999871106}, {"caption": "\\chapterautorefname", "snippet": "\\chapterautorefname", "meta": "aurl-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\TeX", "snippet": "\\TeX", "meta": "aurl-cmd", "score": 0.02873756018238537}, {"caption": "\\TeX{}", "snippet": "\\TeX{$1}", "meta": "aurl-cmd", "score": 0.02873756018238537}, {"caption": "\\protect", "snippet": "\\protect", "meta": "aurl-cmd", "score": 0.0200686676229443}, {"caption": "\\appendixautorefname", "snippet": "\\appendixautorefname", "meta": "aurl-cmd", "score": 7.950698053641679e-05}, {"caption": "\\appendixautorefname{}", "snippet": "\\appendixautorefname{$1}", "meta": "aurl-cmd", "score": 7.950698053641679e-05}, {"caption": "\\newlabel{}{}", "snippet": "\\newlabel{$1}{$2}", "meta": "aurl-cmd", "score": 0.00029737672328168955}, {"caption": "\\texorpdfstring{}{}", "snippet": "\\texorpdfstring{$1}{$2}", "meta": "aurl-cmd", "score": 0.0073781967296121}, {"caption": "\\refstepcounter{}", "snippet": "\\refstepcounter{$1}", "meta": "aurl-cmd", "score": 0.002140559856649122}, {"caption": "\\alph", "snippet": "\\alph", "meta": "aurl-cmd", "score": 0.01034327266194849}, {"caption": "\\alph{}", "snippet": "\\alph{$1}", "meta": "aurl-cmd", "score": 0.01034327266194849}, {"caption": "\\pageref{}", "snippet": "\\pageref{$1}", "meta": "aurl-cmd", "score": 0.019788865471151957}, {"caption": "\\item", "snippet": "\\item", "meta": "aurl-cmd", "score": 3.800886892251021}, {"caption": "\\item[]", "snippet": "\\item[$1]", "meta": "aurl-cmd", "score": 3.800886892251021}, {"caption": "\\LaTeX", "snippet": "\\LaTeX", "meta": "aurl-cmd", "score": 0.2334089308452787}, {"caption": "\\LaTeX{}", "snippet": "\\LaTeX{$1}", "meta": "aurl-cmd", "score": 0.2334089308452787}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\itemautorefname", "snippet": "\\itemautorefname", "meta": "aurl-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\caption{}", "snippet": "\\caption{$1}", "meta": "aurl-cmd", "score": 1.2569477427490174}, {"caption": "\\sectionautorefname", "snippet": "\\sectionautorefname", "meta": "aurl-cmd", "score": 0.0019832324299155183}, {"caption": "\\sectionautorefname{}", "snippet": "\\sectionautorefname{$1}", "meta": "aurl-cmd", "score": 0.0019832324299155183}, {"caption": "\\LaTeXe", "snippet": "\\LaTeXe", "meta": "aurl-cmd", "score": 0.007928096378157487}, {"caption": "\\LaTeXe{}", "snippet": "\\LaTeXe{$1}", "meta": "aurl-cmd", "score": 0.007928096378157487}, {"caption": "\\footref{}", "snippet": "\\footref{$1}", "meta": "aurl-cmd", "score": 0.0003680857021151614}, {"caption": "\\footref", "snippet": "\\footref", "meta": "aurl-cmd", "score": 0.0003680857021151614}, {"caption": "\\hypertarget{}{}", "snippet": "\\hypertarget{$1}{$2}", "meta": "aurl-cmd", "score": 0.009652820108904094}, {"caption": "\\theoremautorefname", "snippet": "\\theoremautorefname", "meta": "aurl-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\maketitle", "snippet": "\\maketitle", "meta": "aurl-cmd", "score": 0.7504160124360846}, {"caption": "\\subparagraphautorefname", "snippet": "\\subparagraphautorefname", "meta": "aurl-cmd", "score": 0.0005446476945175932}, {"caption": "\\url{}", "snippet": "\\url{$1}", "meta": "aurl-cmd", "score": 0.13586474005868793}, {"caption": "\\author{}", "snippet": "\\author{$1}", "meta": "aurl-cmd", "score": 0.8973590434087177}, {"caption": "\\author[]{}", "snippet": "\\author[$1]{$2}", "meta": "aurl-cmd", "score": 0.8973590434087177}, {"caption": "\\href{}{}", "snippet": "\\href{$1}{$2}", "meta": "aurl-cmd", "score": 0.27111130260612365}, {"caption": "\\Roman{}", "snippet": "\\Roman{$1}", "meta": "aurl-cmd", "score": 0.0038703587462843594}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "aurl-cmd", "score": 0.00530510025314411}, {"caption": "\\autoref{}", "snippet": "\\autoref{$1}", "meta": "aurl-cmd", "score": 0.03741172773691362}, {"caption": "\\nolinkurl{}", "snippet": "\\nolinkurl{$1}", "meta": "aurl-cmd", "score": 0.0004995635515943437}, {"caption": "\\end{}", "snippet": "\\end{$1}", "meta": "aurl-cmd", "score": 7.847906405228455}, {"caption": "\\phantomsection", "snippet": "\\phantomsection", "meta": "aurl-cmd", "score": 0.0174633138331273}, {"caption": "\\MakeUppercase{}", "snippet": "\\MakeUppercase{$1}", "meta": "aurl-cmd", "score": 0.006776001543888959}, {"caption": "\\MakeUppercase", "snippet": "\\MakeUppercase", "meta": "aurl-cmd", "score": 0.006776001543888959}, {"caption": "\\partautorefname", "snippet": "\\partautorefname", "meta": "aurl-cmd", "score": 1.8780276211096543e-05}, {"caption": "\\Itemautorefname{}", "snippet": "\\Itemautorefname{$1}", "meta": "aurl-cmd", "score": 6.006262128895586e-05}, {"caption": "\\halign{}", "snippet": "\\halign{$1}", "meta": "aurl-cmd", "score": 0.00017906650306643613}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "aurl-cmd", "score": 0.20852115286477566}, {"caption": "\\ref{}", "snippet": "\\ref{$1}", "meta": "aurl-cmd", "score": 1.4380093454211778}, {"caption": "\\Alph{}", "snippet": "\\Alph{$1}", "meta": "aurl-cmd", "score": 0.002233258780143355}, {"caption": "\\Alph", "snippet": "\\Alph", "meta": "aurl-cmd", "score": 0.002233258780143355}, {"caption": "\\appendix", "snippet": "\\appendix", "meta": "aurl-cmd", "score": 0.047007158741781095}, {"caption": "\\MP", "snippet": "\\MP", "meta": "aurl-cmd", "score": 0.00018344383742255004}, {"caption": "\\MP{}", "snippet": "\\MP{$1}", "meta": "aurl-cmd", "score": 0.00018344383742255004}, {"caption": "\\paragraphautorefname", "snippet": "\\paragraphautorefname", "meta": "aurl-cmd", "score": 0.0005446476945175932}, {"caption": "\\citeN{}", "snippet": "\\citeN{$1}", "meta": "aurl-cmd", "score": 0.0018503938529945614}, {"caption": "\\citeN", "snippet": "\\citeN", "meta": "aurl-cmd", "score": 0.0018503938529945614}, {"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "aurl-cmd", "score": 0.07503475348393239}, {"caption": "\\subsectionautorefname", "snippet": "\\subsectionautorefname", "meta": "aurl-cmd", "score": 0.0012546605780895737}, {"caption": "\\subsectionautorefname{}", "snippet": "\\subsectionautorefname{$1}", "meta": "aurl-cmd", "score": 0.0012546605780895737}, {"caption": "\\hyperref[]{}", "snippet": "\\hyperref[$1]{$2}", "meta": "aurl-cmd", "score": 0.004515152477030062}, {"caption": "\\arabic{}", "snippet": "\\arabic{$1}", "meta": "aurl-cmd", "score": 0.02445837629741638}, {"caption": "\\arabic", "snippet": "\\arabic", "meta": "aurl-cmd", "score": 0.02445837629741638}, {"caption": "\\newline", "snippet": "\\newline", "meta": "aurl-cmd", "score": 0.3311721696201715}, {"caption": "\\hypersetup{}", "snippet": "\\hypersetup{$1}", "meta": "aurl-cmd", "score": 0.06967310843464661}, {"caption": "\\subsubsectionautorefname", "snippet": "\\subsubsectionautorefname", "meta": "aurl-cmd", "score": 0.0012064581899162352}, {"caption": "\\subsubsectionautorefname{}", "snippet": "\\subsubsectionautorefname{$1}", "meta": "aurl-cmd", "score": 0.0012064581899162352}, {"caption": "\\title{}", "snippet": "\\title{$1}", "meta": "aurl-cmd", "score": 0.9202908262245683}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "aurl-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "aurl-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "aurl-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "aurl-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "aurl-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "aurl-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "aurl-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "aurl-cmd", "score": 0.002958865219480927}, {"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "aurl-cmd", "score": 0.00021116765384691477}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "aurl-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "aurl-cmd", "score": 0.00530510025314411}], "bchart": [{"caption": "\\setkeys{}{}", "snippet": "\\setkeys{$1}{$2}", "meta": "bchart-cmd", "score": 0.00037306820619479756}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bchart-cmd", "score": 0.008565354665444157}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bchart-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bchart-cmd", "score": 0.021170869458413965}, {"caption": "\\scalebox{}{}", "snippet": "\\scalebox{$1}{$2}", "meta": "bchart-cmd", "score": 0.015973401906548487}, {"caption": "\\reflectbox{}", "snippet": "\\reflectbox{$1}", "meta": "bchart-cmd", "score": 0.0005981923692899367}, {"caption": "\\reflectbox", "snippet": "\\reflectbox", "meta": "bchart-cmd", "score": 0.0005981923692899367}, {"caption": "\\resizebox{}{}{}", "snippet": "\\resizebox{$1}{$2}{$3}", "meta": "bchart-cmd", "score": 0.017834153815870245}, {"caption": "\\includegraphics[]{}", "snippet": "\\includegraphics[$1]{$2}", "meta": "bchart-cmd", "score": 1.4595731795525781}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bchart-cmd", "score": 0.00530510025314411}, {"caption": "\\DeclareGraphicsExtensions{}", "snippet": "\\DeclareGraphicsExtensions{$1}", "meta": "bchart-cmd", "score": 0.0055519509468004175}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bchart-cmd", "score": 0.008565354665444157}, {"caption": "\\graphicspath{}", "snippet": "\\graphicspath{$1}", "meta": "bchart-cmd", "score": 0.09973951908678011}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "bchart-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "bchart-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "bchart-cmd", "score": 0.004719094298848707}, {"caption": "\\DeclareGraphicsRule{}{}{}{}", "snippet": "\\DeclareGraphicsRule{$1}{$2}{$3}{$4}", "meta": "bchart-cmd", "score": 0.004649150613625593}, {"caption": "\\ifthenelse{}{}{}", "snippet": "\\ifthenelse{$1}{$2}{$3}", "meta": "bchart-cmd", "score": 0.009331077109224957}, {"caption": "\\ifthenelse{}", "snippet": "\\ifthenelse{$1}", "meta": "bchart-cmd", "score": 0.009331077109224957}, {"caption": "\\setboolean{}{}", "snippet": "\\setboolean{$1}{$2}", "meta": "bchart-cmd", "score": 0.0012203054938872515}, {"caption": "\\newboolean{}", "snippet": "\\newboolean{$1}", "meta": "bchart-cmd", "score": 0.0009170966832172938}, {"caption": "\\value{}", "snippet": "\\value{$1}", "meta": "bchart-cmd", "score": 0.01590723355124104}, {"caption": "\\boolean{}", "snippet": "\\boolean{$1}", "meta": "bchart-cmd", "score": 0.0018957469739775527}, {"caption": "\\rotatebox{}{}", "snippet": "\\rotatebox{$1}{$2}", "meta": "bchart-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox[]{}{}", "snippet": "\\rotatebox[$1]{$2}{$3}", "meta": "bchart-cmd", "score": 0.004719094298848707}, {"caption": "\\rotatebox{}", "snippet": "\\rotatebox{$1}", "meta": "bchart-cmd", "score": 0.004719094298848707}, {"caption": "\\definecolors{}", "snippet": "\\definecolors{$1}", "meta": "bchart-cmd", "score": 0.0003209840085766927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "bchart-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "bchart-cmd", "score": 0.021170869458413965}, {"caption": "\\fcolorbox{}{}{}", "snippet": "\\fcolorbox{$1}{$2}{$3}", "meta": "bchart-cmd", "score": 0.00926923425734719}, {"caption": "\\colorlet{}{}", "snippet": "\\colorlet{$1}{$2}", "meta": "bchart-cmd", "score": 0.03654388342026623}, {"caption": "\\textcolor{}{}", "snippet": "\\textcolor{$1}{$2}", "meta": "bchart-cmd", "score": 0.20852115286477566}, {"caption": "\\selectcolormodel{}", "snippet": "\\selectcolormodel{$1}", "meta": "bchart-cmd", "score": 0.000264339771769041}, {"caption": "\\rowcolors{}{}{}", "snippet": "\\rowcolors{$1}{$2}{$3}", "meta": "bchart-cmd", "score": 0.0014120076489723356}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "bchart-cmd", "score": 0.00530510025314411}, {"caption": "\\pagecolor{}", "snippet": "\\pagecolor{$1}", "meta": "bchart-cmd", "score": 0.0008147200475678891}, {"caption": "\\pagecolor{}{}", "snippet": "\\pagecolor{$1}{$2}", "meta": "bchart-cmd", "score": 0.0008147200475678891}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bchart-cmd", "score": 0.008565354665444157}, {"caption": "\\definecolor{}{}{}", "snippet": "\\definecolor{$1}{$2}{$3}", "meta": "bchart-cmd", "score": 0.16906710888680052}, {"caption": "\\colorbox{}{}", "snippet": "\\colorbox{$1}{$2}", "meta": "bchart-cmd", "score": 0.029302172361548254}, {"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "bchart-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "bchart-cmd", "score": 0.2864294797053033}], "pdftexcmds": [{"caption": "\\csname", "snippet": "\\csname", "meta": "pdftexcmds-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "pdftexcmds-cmd", "score": 0.002958865219480927}], "l3keys2e": [{"caption": "\\color[]{}", "snippet": "\\color[$1]{$2}", "meta": "l3keys2e-cmd", "score": 0.2864294797053033}, {"caption": "\\color{}", "snippet": "\\color{$1}", "meta": "l3keys2e-cmd", "score": 0.2864294797053033}], "xfor": [{"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "xfor-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "xfor-cmd", "score": 0.021170869458413965}], "accsupp": [{"caption": "\\RequireXeTeX", "snippet": "\\RequireXeTeX", "meta": "accsupp-cmd", "score": 0.00021116765384691477}, {"caption": "\\empty", "snippet": "\\empty", "meta": "accsupp-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "accsupp-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "accsupp-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "accsupp-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "accsupp-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "accsupp-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "accsupp-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "accsupp-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "accsupp-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "accsupp-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "accsupp-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "accsupp-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "accsupp-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "accsupp-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "accsupp-cmd", "score": 0.021170869458413965}], "trig": [{"caption": "\\csname", "snippet": "\\csname", "meta": "trig-cmd", "score": 0.008565354665444157}], "rerunfilecheck": [{"caption": "\\makeindex", "snippet": "\\makeindex", "meta": "rerunfilecheck-cmd", "score": 0.010304996748556729}, {"caption": "\\index{}", "snippet": "\\index{$1}", "meta": "rerunfilecheck-cmd", "score": 0.013774721817648336}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rerunfilecheck-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "rerunfilecheck-cmd", "score": 0.002958865219480927}, {"caption": "\\clearpage", "snippet": "\\clearpage", "meta": "rerunfilecheck-cmd", "score": 0.1789117552185788}, {"caption": "\\global", "snippet": "\\global", "meta": "rerunfilecheck-cmd", "score": 0.006609629561859019}, {"caption": "\\empty", "snippet": "\\empty", "meta": "rerunfilecheck-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "rerunfilecheck-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rerunfilecheck-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "rerunfilecheck-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "rerunfilecheck-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rerunfilecheck-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "rerunfilecheck-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "rerunfilecheck-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "rerunfilecheck-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rerunfilecheck-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "rerunfilecheck-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "rerunfilecheck-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "rerunfilecheck-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "rerunfilecheck-cmd", "score": 0.021170869458413965}], "pdfescape": [{"caption": "\\empty", "snippet": "\\empty", "meta": "pdfescape-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "pdfescape-cmd", "score": 0.00530510025314411}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "pdfescape-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "pdfescape-cmd", "score": 0.021170869458413965}, {"caption": "\\csname", "snippet": "\\csname", "meta": "pdfescape-cmd", "score": 0.008565354665444157}], "infwarerr": [{"caption": "\\empty", "snippet": "\\empty", "meta": "infwarerr-cmd", "score": 0.002958865219480927}, {"caption": "\\check{}", "snippet": "\\check{$1}", "meta": "infwarerr-cmd", "score": 0.0058342578961340175}, {"caption": "\\space", "snippet": "\\space", "meta": "infwarerr-cmd", "score": 0.023010789853665694}, {"caption": "\\csname", "snippet": "\\csname", "meta": "infwarerr-cmd", "score": 0.008565354665444157}], "kvsetkeys": [{"caption": "\\empty", "snippet": "\\empty", "meta": "kvsetkeys-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "kvsetkeys-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "kvsetkeys-cmd", "score": 0.008565354665444157}], "gettitlestring": [{"caption": "\\addcontentsline{}{}{}", "snippet": "\\addcontentsline{$1}{$2}{$3}", "meta": "gettitlestring-cmd", "score": 0.07503475348393239}, {"caption": "\\empty", "snippet": "\\empty", "meta": "gettitlestring-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "gettitlestring-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "gettitlestring-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "gettitlestring-cmd", "score": 0.002958865219480927}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "gettitlestring-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "gettitlestring-cmd", "score": 0.008565354665444157}, {"caption": "\\csname", "snippet": "\\csname", "meta": "gettitlestring-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "gettitlestring-cmd", "score": 0.002958865219480927}, {"caption": "\\expandafter", "snippet": "\\expandafter", "meta": "gettitlestring-cmd", "score": 0.021170869458413965}, {"caption": "\\expandafter{}", "snippet": "\\expandafter{$1}", "meta": "gettitlestring-cmd", "score": 0.021170869458413965}], "refcount": [{"caption": "\\thepage", "snippet": "\\thepage", "meta": "refcount-cmd", "score": 0.0591555998103519}], "bitset": [{"caption": "\\empty", "snippet": "\\empty", "meta": "bitset-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "bitset-cmd", "score": 0.008565354665444157}], "etexcmds": [{"caption": "\\csname", "snippet": "\\csname", "meta": "etexcmds-cmd", "score": 0.008565354665444157}, {"caption": "\\empty", "snippet": "\\empty", "meta": "etexcmds-cmd", "score": 0.002958865219480927}], "intcalc": [{"caption": "\\empty", "snippet": "\\empty", "meta": "intcalc-cmd", "score": 0.002958865219480927}, {"caption": "\\csname", "snippet": "\\csname", "meta": "intcalc-cmd", "score": 0.008565354665444157}], "hycolor": [{"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hycolor-cmd", "score": 0.00530510025314411}, {"caption": "\\csname", "snippet": "\\csname", "meta": "hycolor-cmd", "score": 0.008565354665444157}, {"caption": "\\noexpand", "snippet": "\\noexpand", "meta": "hycolor-cmd", "score": 0.00530510025314411}]} \ No newline at end of file diff --git a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee b/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee deleted file mode 100644 index d32ba6233f..0000000000 --- a/services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee +++ /dev/null @@ -1,89 +0,0 @@ -async = require('async') -logger = require 'logger-sharelatex' -Settings = require 'settings-sharelatex' -crypto = require('crypto') -Mailchimp = require('mailchimp-api-v3') - -if !Settings.mailchimp?.api_key? - logger.info "Using newsletter provider: none" - mailchimp = - request: (opts, cb)-> cb() -else - logger.info "Using newsletter provider: mailchimp" - mailchimp = new Mailchimp(Settings.mailchimp?.api_key) - -module.exports = - - subscribe: (user, callback = () ->)-> - options = buildOptions(user, true) - logger.log options:options, user:user, email:user.email, "subscribing user to the mailing list" - mailchimp.request options, (err)-> - if err? - logger.err err:err, user:user, "error subscribing person to newsletter" - else - logger.log user:user, "finished subscribing user to the newsletter" - callback(err) - - unsubscribe: (user, callback = () ->)-> - logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list" - options = buildOptions(user, false) - mailchimp.request options, (err)-> - if err? - logger.err err:err, user:user, "error unsubscribing person to newsletter" - else - logger.log user:user, "finished unsubscribing user to the newsletter" - callback(err) - - changeEmail: (oldEmail, newEmail, callback = ()->)-> - options = buildOptions({email:oldEmail}) - delete options.body.status - options.body.email_address = newEmail - logger.log {oldEmail, newEmail, options}, "changing email in newsletter" - mailchimp.request options, (err)-> - if err? and err?.message?.indexOf("merge fields were invalid") != -1 - logger.log {oldEmail, newEmail}, "unable to change email in newsletter, user has never subscribed" - return callback() - else if err? and err?.message?.indexOf("could not be validated") != -1 - logger.log {oldEmail, newEmail}, - "unable to change email in newsletter, user has previously unsubscribed or new email already exist on list" - return callback() - else if err? and err.message.indexOf("is already a list member") != -1 - logger.log {oldEmail, newEmail}, - "unable to change email in newsletter, new email is already on mailing list" - return callback() - else if err? and err?.message?.indexOf("looks fake or invalid") != -1 - logger.log {oldEmail, newEmail}, - "unable to change email in newsletter, email looks fake to mailchimp" - return callback() - else if err? - logger.err {err, oldEmail, newEmail}, "error changing email in newsletter" - return callback(err) - else - logger.log "finished changing email in the newsletter" - return callback() - -hashEmail = (email)-> - crypto.createHash('md5').update(email.toLowerCase()).digest("hex") - -buildOptions = (user, is_subscribed)-> - subscriber_hash = hashEmail(user.email) - status = if is_subscribed then "subscribed" else "unsubscribed" - opts = - method: "PUT" - path: "/lists/#{Settings.mailchimp?.list_id}/members/#{subscriber_hash}" - body: - email_address:user.email - status_if_new: status - - #only set status if we explictly want to set it - if is_subscribed? - opts.body.status = status - - if user._id? - opts.body.merge_fields = - FNAME: user.first_name - LNAME: user.last_name - MONGO_ID:user._id - - return opts - diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee deleted file mode 100644 index b787bcf91c..0000000000 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ /dev/null @@ -1,63 +0,0 @@ -logger = require("logger-sharelatex") -NotificationsHandler = require("./NotificationsHandler") -request = require "request" -settings = require "settings-sharelatex" - -module.exports = -# Note: notification keys should be url-safe - - featuresUpgradedByAffiliation: (affiliation, user) -> - key: "features-updated-by=#{affiliation.institutionId}" - create: (callback=()->) -> - messageOpts = - institutionName: affiliation.institutionName - NotificationsHandler.createNotification user._id, @key, "notification_features_upgraded_by_affiliation", messageOpts, null, false, callback - read: (callback=()->) -> - NotificationsHandler.markAsRead @key, callback - - redundantPersonalSubscription: (affiliation, user) -> - key: "redundant-personal-subscription-#{affiliation.institutionId}" - create: (callback=()->) -> - messageOpts = - institutionName: affiliation.institutionName - NotificationsHandler.createNotification user._id, @key, "notification_personal_subscription_not_required_due_to_affiliation", messageOpts, null, false, callback - read: (callback=()->) -> - NotificationsHandler.markAsRead @key, callback - - projectInvite: (invite, project, sendingUser, user) -> - key: "project-invite-#{invite._id}" - create: (callback=()->) -> - messageOpts = - userName: sendingUser.first_name - projectName: project.name - projectId: project._id.toString() - token: invite.token - logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user" - NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback - read: (callback=()->) -> - NotificationsHandler.markAsReadByKeyOnly @key, callback - - ipMatcherAffiliation: (userId) -> - create: (ip, callback=()->) -> - return null unless settings?.apis?.v1?.url # service is not configured - request { - method: 'GET' - url: "#{settings.apis.v1.url}/api/v2/users/#{userId}/ip_matcher" - auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } - body: { ip: ip } - json: true - timeout: 20 * 1000 - }, (error, response, body) -> - return error if error? - return null unless response.statusCode == 200 - - key = "ip-matched-affiliation-#{body.id}" - messageOpts = - university_name: body.name - content: body.enrolment_ad_html - logger.log user_id:userId, key:key, "creating notification key for user" - NotificationsHandler.createNotification userId, key, "notification_ip_matched_affiliation", messageOpts, null, false, callback - - read: (university_id, callback = ->)-> - key = "ip-matched-affiliation-#{university_id}" - NotificationsHandler.markAsReadWithKey userId, key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee deleted file mode 100644 index 5b83a60248..0000000000 --- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee +++ /dev/null @@ -1,21 +0,0 @@ -NotificationsHandler = require("./NotificationsHandler") -AuthenticationController = require("../Authentication/AuthenticationController") -logger = require("logger-sharelatex") -_ = require("underscore") - -module.exports = - - getAllUnreadNotifications: (req, res)-> - user_id = AuthenticationController.getLoggedInUserId(req) - NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)-> - unreadNotifications = _.map unreadNotifications, (notification)-> - notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts) - return notification - res.send(unreadNotifications) - - markNotificationAsRead: (req, res)-> - user_id = AuthenticationController.getLoggedInUserId(req) - notification_id = req.params.notification_id - NotificationsHandler.markAsRead user_id, notification_id, -> - res.send() - logger.log user_id:user_id, notification_id:notification_id, "mark notification as read" diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee deleted file mode 100644 index a0f6ae5c12..0000000000 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ /dev/null @@ -1,80 +0,0 @@ -settings = require("settings-sharelatex") -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}" - json: true - timeout: oneSecond - 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}") - logger.err err:err, "something went wrong getting notifications" - callback(null, []) - else - if !unreadNotifications? - unreadNotifications = [] - callback(null, unreadNotifications) - - createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)-> - if !callback - callback = forceCreate - forceCreate = true - payload = { - key:key - messageOpts:messageOpts - templateKey:templateKey - forceCreate:forceCreate - } - if expiryDateTime? - payload.expires = expiryDateTime - opts = - uri: "#{settings.apis.notifications?.url}/user/#{user_id}" - timeout: oneSecond - method:"POST" - json: payload - logger.log opts:opts, "creating notification for user" - makeRequest opts, callback - - markAsReadWithKey: (user_id, key, callback)-> - opts = - 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" - makeRequest opts, callback - - - markAsRead: (user_id, notification_id, callback)-> - opts = - 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" - makeRequest opts, callback - - # removes notification by key, without regard for user_id, - # should not be exposed to user via ui/router - markAsReadByKeyOnly: (key, callback)-> - opts = - uri: "#{settings.apis.notifications?.url}/key/#{key}" - method: "DELETE" - timeout: oneSecond - logger.log {key:key}, "sending mark notification as read with key-only to notifications api" - makeRequest opts, callback diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee deleted file mode 100644 index d448a843cb..0000000000 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee +++ /dev/null @@ -1,83 +0,0 @@ -PasswordResetHandler = require("./PasswordResetHandler") -RateLimiter = require("../../infrastructure/RateLimiter") -AuthenticationController = require("../Authentication/AuthenticationController") -AuthenticationManager = require("../Authentication/AuthenticationManager") -UserGetter = require("../User/UserGetter") -UserUpdater = require("../User/UserUpdater") -UserSessionsManager = require("../User/UserSessionsManager") -logger = require "logger-sharelatex" -Settings = require 'settings-sharelatex' - -module.exports = - - renderRequestResetForm: (req, res)-> - logger.log "rendering request reset form" - res.render "user/passwordReset", - title:"reset_password" - - requestReset: (req, res)-> - email = req.body.email.trim().toLowerCase() - opts = - endpointName: "password_reset_rate_limit" - timeInterval: 60 - subjectName: req.ip - throttle: 6 - RateLimiter.addCount opts, (err, canContinue)-> - if !canContinue - return res.send 429, { message: req.i18n.translate("rate_limit_hit_wait")} - PasswordResetHandler.generateAndEmailResetToken email, (err, status)-> - if err? - res.send 500, {message:err?.message} - else if status == 'primary' - res.send 200, {message: {text: req.i18n.translate("password_reset_email_sent")}} - else if status == 'secondary' - res.send 404, {message: req.i18n.translate("secondary_email_password_reset")} - else if status == 'sharelatex' - res.send 404, {message: "#{req.i18n.translate("reset_from_sl")}"} - else - res.send 404, {message: req.i18n.translate("cant_find_email")} - - renderSetPasswordForm: (req, res)-> - if req.query.passwordResetToken? - req.session.resetToken = req.query.passwordResetToken - return res.redirect('/user/password/set') - if !req.session.resetToken? - return res.redirect('/user/password/reset') - res.render "user/setPassword", - title:"set_password" - passwordResetToken: req.session.resetToken - - setNewUserPassword: (req, res, next)-> - {passwordResetToken, password} = req.body - if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0 or AuthenticationManager.validatePassword(password?.trim())? - return res.sendStatus 400 - delete req.session.resetToken - PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) -> - if err and err.name and err.name == "NotFoundError" - res.status(404).send("NotFoundError") - else if err and err.name and err.name == "NotInV2Error" - res.status(403).send("NotInV2Error") - else if err and err.name and err.name == "SLInV2Error" - res.status(403).send("SLInV2Error") - else if err and err.statusCode and err.statusCode == 500 - res.status(500) - else if err and !err.statusCode - res.status(500) - else if found - return res.sendStatus 200 if !user_id? # will not exist for v1-only users - UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) -> - return next(err) if err? - UserUpdater.removeReconfirmFlag 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? - AuthenticationController.afterLoginSessionSetup req, user, (err) -> - if err? - logger.err {err, email: user.email}, "Error setting up session after setting password" - return next(err) - res.json {redir: AuthenticationController._getRedirectFromSession(req) || "/project"} - else - res.sendStatus 200 - else - res.sendStatus 404 diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee deleted file mode 100644 index 169482ed9b..0000000000 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee +++ /dev/null @@ -1,76 +0,0 @@ -settings = require("settings-sharelatex") -async = require("async") -UserGetter = require("../User/UserGetter") -OneTimeTokenHandler = require("../Security/OneTimeTokenHandler") -EmailHandler = require("../Email/EmailHandler") -AuthenticationManager = require("../Authentication/AuthenticationManager") -logger = require("logger-sharelatex") -V1Api = require("../V1/V1Api") - -module.exports = PasswordResetHandler = - - generateAndEmailResetToken:(email, callback = (error, status) ->)-> - PasswordResetHandler._getPasswordResetData email, (error, exists, data) -> - if error? - return callback(error, null) - else if exists - OneTimeTokenHandler.getNewToken 'password', data, (err, token)-> - if err then return callback(err) - emailOptions = - to : email - setNewPasswordUrl : "#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}&email=#{encodeURIComponent(email)}" - EmailHandler.sendEmail "passwordResetRequested", emailOptions, (error) -> - return callback(error) if error? - callback null, 'primary' - else - UserGetter.getUserByAnyEmail email, (err, user) -> - if !user - return callback(error, null) - else if !user.overleaf?.id? - return callback(error, 'sharelatex') - else - return callback(error, 'secondary') - - setNewUserPassword: (token, password, callback = (error, found, user_id) ->)-> - OneTimeTokenHandler.getValueFromTokenAndExpire 'password', token, (err, data)-> - if err then return callback(err) - if !data? - return callback null, false, null - if typeof data == "string" - # Backwards compatible with old format. - # Tokens expire after 1h, so this can be removed soon after deploy. - # Possibly we should keep this until we do an onsite release too. - data = { user_id: data } - if data.user_id? - AuthenticationManager.setUserPassword data.user_id, password, (err, reset) -> - if err then return callback(err) - callback null, reset, data.user_id - else if data.v1_user_id? - AuthenticationManager.setUserPasswordInV1 data.v1_user_id, password, (error, reset) -> - return callback(error) if error? - UserGetter.getUser { 'overleaf.id': data.v1_user_id }, {_id:1}, (error, user) -> - return callback(error) if error? - callback null, reset, user?._id - - _getPasswordResetData: (email, callback = (error, exists, data) ->) -> - if settings.overleaf? - # Overleaf v2 - V1Api.request { - url: "/api/v1/sharelatex/user_emails" - qs: - email: email - expectedStatusCodes: [404] - }, (error, response, body) -> - return callback(error) if error? - if response.statusCode == 404 - return callback null, false - else - return callback null, true, { v1_user_id: body.user_id } - else - # ShareLaTeX - UserGetter.getUserByMainEmail email, (err, user)-> - if err then return callback(err) - if !user? or user.holdingAccount or user.overleaf? - logger.err email:email, "user could not be found for password reset" - return callback(null, false) - return callback null, true, { user_id: user._id } diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee deleted file mode 100644 index d3591f8623..0000000000 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee +++ /dev/null @@ -1,15 +0,0 @@ -PasswordResetController = require("./PasswordResetController") -AuthenticationController = require('../Authentication/AuthenticationController') - -module.exports = - apply: (webRouter, apiRouter) -> - - webRouter.get '/user/password/reset', PasswordResetController.renderRequestResetForm - webRouter.post '/user/password/reset', PasswordResetController.requestReset - AuthenticationController.addEndpointToLoginWhitelist '/user/password/reset' - - webRouter.get '/user/password/set', PasswordResetController.renderSetPasswordForm - webRouter.post '/user/password/set', PasswordResetController.setNewUserPassword - AuthenticationController.addEndpointToLoginWhitelist '/user/password/set' - - webRouter.post '/user/reconfirm', PasswordResetController.requestReset \ No newline at end of file diff --git a/services/web/app/coffee/Features/Project/DocLinesComparitor.coffee b/services/web/app/coffee/Features/Project/DocLinesComparitor.coffee deleted file mode 100644 index 4a286e66c7..0000000000 --- a/services/web/app/coffee/Features/Project/DocLinesComparitor.coffee +++ /dev/null @@ -1,10 +0,0 @@ -_ = require "underscore" - -module.exports = - - areSame: (lines1, lines2)-> - if !Array.isArray(lines1) or !Array.isArray(lines2) - return false - - return _.isEqual(lines1, lines2) - diff --git a/services/web/app/coffee/Features/Project/ProjectApiController.coffee b/services/web/app/coffee/Features/Project/ProjectApiController.coffee deleted file mode 100644 index f832a75b54..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectApiController.coffee +++ /dev/null @@ -1,12 +0,0 @@ -ProjectDetailsHandler = require("./ProjectDetailsHandler") -logger = require("logger-sharelatex") - - -module.exports = - - getProjectDetails : (req, res, next)-> - {project_id} = req.params - ProjectDetailsHandler.getDetails project_id, (err, projDetails)-> - return next(err) if err? - res.json(projDetails) - diff --git a/services/web/app/coffee/Features/Project/ProjectCollabratecDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCollabratecDetailsHandler.coffee deleted file mode 100644 index e4b47c9da9..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectCollabratecDetailsHandler.coffee +++ /dev/null @@ -1,73 +0,0 @@ -ObjectId = require("mongojs").ObjectId -Project = require("../../models/Project").Project - -module.exports = ProjectCollabratecDetailsHandler = - initializeCollabratecProject: (project_id, user_id, collabratec_document_id, collabratec_privategroup_id, callback=(err)->) -> - ProjectCollabratecDetailsHandler.setCollabratecUsers project_id, [ { user_id, collabratec_document_id, collabratec_privategroup_id } ], callback - - isLinkedCollabratecUserProject: (project_id, user_id, callback=(err, isLinked)->) -> - try - project_id = ObjectId project_id - user_id = ObjectId user_id - catch err - return callback err - query = - _id: project_id - collabratecUsers: $elemMatch: - user_id: user_id - Project.findOne query, {_id: 1}, (err, project) -> - callback err if err? - callback null, project? - - linkCollabratecUserProject: (project_id, user_id, collabratec_document_id, callback=(err)->) -> - try - project_id = ObjectId project_id - user_id = ObjectId user_id - catch err - return callback err - query = - _id: project_id - collabratecUsers: $not: $elemMatch: - collabratec_document_id: collabratec_document_id - user_id: user_id - update = $push: collabratecUsers: - collabratec_document_id: collabratec_document_id - user_id: user_id - Project.update query, update, callback - - setCollabratecUsers: (project_id, collabratec_users, callback=(err)->) -> - try - project_id = ObjectId project_id - catch err - return callback err - callback(new Error "collabratec_users must be array") unless Array.isArray(collabratec_users) - for collabratec_user in collabratec_users - try - collabratec_user.user_id = ObjectId(collabratec_user.user_id) - catch err - return callback err - update = $set: { collabratecUsers: collabratec_users } - Project.update { _id: project_id }, update, callback - - unlinkCollabratecUserProject: (project_id, user_id, callback=(err)->) -> - try - project_id = ObjectId project_id - user_id = ObjectId user_id - catch err - return callback err - query = - _id: project_id - update = $pull: collabratecUsers: - user_id: user_id - Project.update query, update, callback - - updateCollabratecUserIds: (old_user_id, new_user_id, callback=(err)->) -> - try - old_user_id = ObjectId old_user_id - new_user_id = ObjectId new_user_id - catch err - return callback err - query = "collabratecUsers.user_id": old_user_id - update = $set: "collabratecUsers.$.user_id": new_user_id - options = multi: true - Project.update query, update, options, callback diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee deleted file mode 100644 index 508c742299..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ /dev/null @@ -1,533 +0,0 @@ -async = require("async") -logger = require("logger-sharelatex") -projectDeleter = require("./ProjectDeleter") -projectDuplicator = require("./ProjectDuplicator") -projectCreationHandler = require("./ProjectCreationHandler") -editorController = require("../Editor/EditorController") -metrics = require('metrics-sharelatex') -User = require('../../models/User').User -TagsHandler = require("../Tags/TagsHandler") -SubscriptionLocator = require("../Subscription/SubscriptionLocator") -NotificationsHandler = require("../Notifications/NotificationsHandler") -LimitationsManager = require("../Subscription/LimitationsManager") -underscore = require("underscore") -Settings = require("settings-sharelatex") -AuthorizationManager = require("../Authorization/AuthorizationManager") -fs = require "fs" -InactiveProjectManager = require("../InactiveData/InactiveProjectManager") -ProjectUpdateHandler = require("./ProjectUpdateHandler") -ProjectGetter = require("./ProjectGetter") -PrivilegeLevels = require("../Authorization/PrivilegeLevels") -AuthenticationController = require("../Authentication/AuthenticationController") -PackageVersions = require("../../infrastructure/PackageVersions") -AnalyticsManager = require "../Analytics/AnalyticsManager" -Sources = require "../Authorization/Sources" -TokenAccessHandler = require '../TokenAccess/TokenAccessHandler' -CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler' -Modules = require '../../infrastructure/Modules' -ProjectEntityHandler = require './ProjectEntityHandler' -UserGetter = require("../User/UserGetter") -NotificationsBuilder = require("../Notifications/NotificationsBuilder") -crypto = require 'crypto' -{ V1ConnectionError } = require '../Errors/Errors' -Features = require('../../infrastructure/Features') -BrandVariationsHandler = require("../BrandVariations/BrandVariationsHandler") -{ getUserAffiliations } = require("../Institutions/InstitutionsAPI") -V1Handler = require "../V1/V1Handler" - -module.exports = ProjectController = - - _isInPercentageRollout: (rolloutName, objectId, percentage) -> - if Settings.bypassPercentageRollouts == true - return true - data = "#{rolloutName}:#{objectId.toString()}" - md5hash = crypto.createHash('md5').update(data).digest('hex') - counter = parseInt(md5hash.slice(26, 32), 16) - return (counter % 100) < percentage - - updateProjectSettings: (req, res, next) -> - project_id = req.params.Project_id - - jobs = [] - - if req.body.compiler? - jobs.push (callback) -> - editorController.setCompiler project_id, req.body.compiler, callback - - if req.body.imageName? - jobs.push (callback) -> - editorController.setImageName project_id, req.body.imageName, callback - - if req.body.name? - jobs.push (callback) -> - editorController.renameProject project_id, req.body.name, callback - - if req.body.spellCheckLanguage? - jobs.push (callback) -> - editorController.setSpellCheckLanguage project_id, req.body.spellCheckLanguage, callback - - if req.body.rootDocId? - jobs.push (callback) -> - editorController.setRootDoc project_id, req.body.rootDocId, callback - - async.series jobs, (error) -> - return next(error) if error? - res.sendStatus(204) - - updateProjectAdminSettings: (req, res, next) -> - project_id = req.params.Project_id - - jobs = [] - if req.body.publicAccessLevel? - jobs.push (callback) -> - editorController.setPublicAccessLevel project_id, req.body.publicAccessLevel, callback - - async.series jobs, (error) -> - return next(error) if error? - res.sendStatus(204) - - deleteProject: (req, res) -> - project_id = req.params.Project_id - forever = req.query?.forever? - logger.log project_id: project_id, forever: forever, "received request to archive project" - user = AuthenticationController.getSessionUser(req) - cb = (err)-> - if err? - res.sendStatus 500 - else - res.sendStatus 200 - - if forever - projectDeleter.deleteProject project_id, {deleterUser: user, ipAddress:req.ip} , cb - else - projectDeleter.archiveProject project_id, cb - - restoreProject: (req, res) -> - project_id = req.params.Project_id - logger.log project_id:project_id, "received request to restore project" - projectDeleter.restoreProject project_id, (err)-> - if err? - res.sendStatus 500 - else - res.sendStatus 200 - - cloneProject: (req, res, next)-> - res.setTimeout(5 * 60 * 1000) # allow extra time for the copy to complete - metrics.inc "cloned-project" - project_id = req.params.Project_id - projectName = req.body.projectName - logger.log project_id:project_id, projectName:projectName, "cloning project" - if !AuthenticationController.isUserLoggedIn(req) - return res.send redir:"/register" - currentUser = AuthenticationController.getSessionUser(req) - projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)-> - if err? - logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project" - return next(err) - res.send({name:project.name, project_id:project._id, owner_ref:project.owner_ref}) - - - newProject: (req, res, next)-> - user_id = AuthenticationController.getLoggedInUserId(req) - projectName = req.body.projectName?.trim() - template = req.body.template - logger.log user: user_id, projectType: template, name: projectName, "creating project" - async.waterfall [ - (cb)-> - if template == 'example' - projectCreationHandler.createExampleProject user_id, projectName, cb - else - projectCreationHandler.createBasicProject user_id, projectName, cb - ], (err, project)-> - return next(err) if err? - logger.log project: project, user: user_id, name: projectName, templateType: template, "created project" - res.send {project_id:project._id} - - - renameProject: (req, res, next)-> - project_id = req.params.Project_id - newName = req.body.newProjectName - editorController.renameProject project_id, newName, (err)-> - return next(err) if err? - res.sendStatus 200 - - userProjectsJson: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - ProjectGetter.findAllUsersProjects user_id, - 'name lastUpdated publicAccesLevel archived owner_ref tokens', (err, projects) -> - return next(err) if err? - projects = ProjectController._buildProjectList(projects) - .filter((p) -> !p.archived) - .filter((p) -> !p.isV1Project) - .map((p) -> {_id: p.id, name: p.name, accessLevel: p.accessLevel}) - - res.json({projects: projects}) - - projectEntitiesJson: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - project_id = req.params.Project_id - ProjectGetter.getProject project_id, (err, project) -> - return next(err) if err? - ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> - return next(err) if err? - entities = docs.concat(files) - .sort (a, b) -> a.path > b.path # Sort by path ascending - .map (e) -> { - path: e.path, - type: if e.doc? then 'doc' else 'file' - } - res.json({project_id: project_id, entities: entities}) - - projectListPage: (req, res, next)-> - timer = new metrics.Timer("project-list") - user_id = AuthenticationController.getLoggedInUserId(req) - currentUser = AuthenticationController.getSessionUser(req) - async.parallel { - tags: (cb)-> - TagsHandler.getAllTags user_id, cb - notifications: (cb)-> - NotificationsHandler.getUserNotifications user_id, cb - projects: (cb)-> - ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated lastUpdatedBy publicAccesLevel archived owner_ref tokens', cb - v1Projects: (cb) -> - Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) -> - if error? and error instanceof V1ConnectionError - return cb(null, projects: [], tags: [], noConnection: true) - return cb(error, projects[0]) # hooks.fire returns an array of results, only need first - hasSubscription: (cb)-> - LimitationsManager.hasPaidSubscription currentUser, (error, hasPaidSubscription) -> - if error? and error instanceof V1ConnectionError - return cb(null, true) - return cb(error, hasPaidSubscription) - user: (cb) -> - User.findById user_id, "featureSwitches overleaf awareOfV2 features", cb - userAffiliations: (cb) -> - getUserAffiliations user_id, cb - }, (err, results)-> - if err? - logger.err err:err, "error getting data for project list page" - return next(err) - logger.log results:results, user_id:user_id, "rendering project list" - v1Tags = results.v1Projects?.tags or [] - tags = results.tags[0].concat(v1Tags) - notifications = require("underscore").map results.notifications, (notification)-> - notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts) - return notification - portalTemplates = ProjectController._buildPortalTemplatesList results.userAffiliations - projects = ProjectController._buildProjectList results.projects, results.v1Projects?.projects - user = results.user - userAffiliations = results.userAffiliations - warnings = ProjectController._buildWarningsList results.v1Projects - - # in v2 add notifications for matching university IPs - if Settings.overleaf? - UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) -> - if req.ip != user.lastLoginIp - NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create() - - ProjectController._injectProjectUsers projects, (error, projects) -> - return next(error) if error? - viewModel = { - title:'your_projects' - priority_title: true - projects: projects - tags: tags - notifications: notifications or [] - portalTemplates: portalTemplates - user: user - userAffiliations: userAffiliations - hasSubscription: results.hasSubscription - isShowingV1Projects: results.v1Projects? - warnings: warnings - zipFileSizeLimit: Settings.maxUploadSize - } - - if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key? - viewModel.showUserDetailsArea = true - viewModel.algolia_api_key = Settings.algolia.read_only_api_key - viewModel.algolia_app_id = Settings.algolia.app_id - else - viewModel.showUserDetailsArea = false - - paidUser = user.features?.github and user.features?.dropbox # use a heuristic for paid account - freeUserProportion = 0.10 - sampleFreeUser = parseInt(user._id.toString().slice(-2), 16) < freeUserProportion * 255 - showFrontWidget = paidUser or sampleFreeUser - logger.log {paidUser, sampleFreeUser, showFrontWidget}, 'deciding whether to show front widget' - if showFrontWidget - viewModel.frontChatWidgetRoomId = Settings.overleaf?.front_chat_widget_room_id - - res.render 'project/list', viewModel - timer.done() - - - loadEditor: (req, res, next)-> - timer = new metrics.Timer("load-editor") - if !Settings.editorIsOpen - return res.render("general/closed", {title:"updating_site"}) - - if AuthenticationController.isUserLoggedIn(req) - user_id = AuthenticationController.getLoggedInUserId(req) - anonymous = false - else - anonymous = true - user_id = null - - project_id = req.params.Project_id - logger.log project_id:project_id, anonymous:anonymous, user_id:user_id, "loading editor" - - # record failures to load the custom websocket - if req.query?.ws is 'fallback' - metrics.inc "load-editor-ws-fallback" - - async.auto { - project: (cb)-> - ProjectGetter.getProject( - project_id, - { name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1, brandVariationId: 1, overleaf: 1, tokens: 1 }, - (err, project) -> - return cb(err) if err? - return cb(null, project) unless project.overleaf?.id? and project.tokens?.readAndWrite? and Settings.projectImportingCheckMaxCreateDelta? - createDelta = (new Date().getTime() - new Date(project._id.getTimestamp()).getTime()) / 1000 - return cb(null, project) unless createDelta < Settings.projectImportingCheckMaxCreateDelta - V1Handler.getDocExported project.tokens.readAndWrite, (err, doc_exported) -> - return next err if err? - project.exporting = doc_exported.exporting - cb(null, project) - ) - user: (cb)-> - if !user_id? - cb null, defaultSettingsForAnonymousUser(user_id) - else - User.findById user_id, (err, user)-> - logger.log project_id:project_id, user_id:user_id, "got user" - cb err, user - subscription: (cb)-> - if !user_id? - return cb() - SubscriptionLocator.getUsersSubscription user_id, cb - activate: (cb)-> - InactiveProjectManager.reactivateProjectIfRequired project_id, cb - markAsOpened: (cb)-> - #don't need to wait for this to complete - ProjectUpdateHandler.markAsOpened project_id, -> - cb() - isTokenMember: (cb) -> - cb = underscore.once(cb) - if !user_id? - return cb() - CollaboratorsHandler.userIsTokenMember user_id, project_id, cb - brandVariation: [ "project", (cb, results) -> - if !results.project?.brandVariationId? - return cb() - BrandVariationsHandler.getBrandVariationById results.project.brandVariationId, (error, brandVariationDetails) -> - cb(error, brandVariationDetails) - ] - }, (err, results)-> - if err? - logger.err err:err, "error getting details for project page" - return next err - project = results.project - user = results.user - subscription = results.subscription - brandVariation = results.brandVariation - - daysSinceLastUpdated = (new Date() - project.lastUpdated) / 86400000 - logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" - - token = TokenAccessHandler.getRequestToken(req, project_id) - isTokenMember = results.isTokenMember - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel)-> - return next(error) if error? - if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE - return res.sendStatus 401 - - if project.exporting - res.render 'project/importing', - bodyClasses: ["editor"] - return - - if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? - allowedFreeTrial = !!subscription.freeTrial.allowed || true - - logger.log project_id:project_id, "rendering editor page" - res.render 'project/editor', - title: project.name - priority_title: true - bodyClasses: ["editor"] - project_id : project._id - user : { - id : user_id - email : user.email - 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 - isAdmin: user.isAdmin - } - userSettings: { - mode : user.ace.mode - editorTheme : user.ace.theme - fontSize : user.ace.fontSize - autoComplete: user.ace.autoComplete - autoPairDelimiters: user.ace.autoPairDelimiters - pdfViewer : user.ace.pdfViewer - syntaxValidation: user.ace.syntaxValidation - fontFamily: user.ace.fontFamily - lineHeight: user.ace.lineHeight - overallTheme: user.ace.overallTheme - } - trackChangesState: project.track_changes - privilegeLevel: privilegeLevel - chatUrl: Settings.apis.chat.url - anonymous: anonymous - anonymousAccessToken: req._anonymousAccessToken - isTokenMember: isTokenMember - languages: Settings.languages - editorThemes: THEME_LIST - maxDocLength: Settings.max_doc_length - useV2History: !!project.overleaf?.history?.display - richTextTrackChangesEnabled: req.query?.rttc == 'true' || user.betaProgram - showTestControls: req.query?.tc == 'true' || user.isAdmin - brandVariation: brandVariation - allowedImageNames: Settings.allowedImageNames || [] - gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl - timer.done() - - _buildProjectList: (allProjects, v1Projects = [])-> - {owned, readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly} = allProjects - projects = [] - for project in owned - projects.push ProjectController._buildProjectViewModel(project, "owner", Sources.OWNER) - # Invite-access - for project in readAndWrite - projects.push ProjectController._buildProjectViewModel(project, "readWrite", Sources.INVITE) - for project in readOnly - projects.push ProjectController._buildProjectViewModel(project, "readOnly", Sources.INVITE) - for project in v1Projects - projects.push ProjectController._buildV1ProjectViewModel(project) - # Token-access - # Only add these projects if they're not already present, this gives us cascading access - # from 'owner' => 'token-read-only' - for project in tokenReadAndWrite - if projects.filter((p) -> p.id.toString() == project._id.toString()).length == 0 - projects.push ProjectController._buildProjectViewModel(project, "readAndWrite", Sources.TOKEN) - for project in tokenReadOnly - if projects.filter((p) -> p.id.toString() == project._id.toString()).length == 0 - projects.push ProjectController._buildProjectViewModel(project, "readOnly", Sources.TOKEN) - - return projects - - _buildProjectViewModel: (project, accessLevel, source) -> - TokenAccessHandler.protectTokens(project, accessLevel) - model = { - id: project._id - name: project.name - lastUpdated: project.lastUpdated - lastUpdatedBy: project.lastUpdatedBy - publicAccessLevel: project.publicAccesLevel - accessLevel: accessLevel - source: source - archived: !!project.archived - owner_ref: project.owner_ref - tokens: project.tokens - isV1Project: false - } - return model - - _buildV1ProjectViewModel: (project) -> - projectViewModel = { - id: project.id - name: project.title - lastUpdated: new Date(project.updated_at * 1000) # Convert from epoch - archived: project.removed || project.archived - isV1Project: true - } - if (project.owner? and project.owner.user_is_owner) or (project.creator? and project.creator.user_is_creator) - projectViewModel.accessLevel = "owner" - else - projectViewModel.accessLevel = "readOnly" - if project.owner? - projectViewModel.owner = { - first_name: project.owner.name - } - else if project.creator? - projectViewModel.owner = { - first_name: project.creator.name - } - return projectViewModel - - - _injectProjectUsers: (projects, callback = (error, projects) ->) -> - users = {} - for project in projects - if project.owner_ref? - users[project.owner_ref.toString()] = true - if project.lastUpdatedBy? - users[project.lastUpdatedBy.toString()] = true - - jobs = [] - for user_id, _ of users - do (user_id) -> - jobs.push (callback) -> - UserGetter.getUserOrUserStubById user_id, { first_name: 1, last_name: 1, email: 1 }, (error, user) -> - return callback(error) if error? - users[user_id] = user - callback() - - async.series jobs, (error) -> - for project in projects - if project.owner_ref? - project.owner = users[project.owner_ref.toString()] - if project.lastUpdatedBy? - project.lastUpdatedBy = users[project.lastUpdatedBy.toString()] or null - callback null, projects - - _buildWarningsList: (v1ProjectData = {}) -> - warnings = [] - if v1ProjectData.noConnection - warnings.push 'No V1 Connection' - if v1ProjectData.hasHiddenV1Projects - warnings.push "Looks like you've got a lot of V1 projects! Some of them may be hidden on V2. To view them all, use the V1 dashboard." - return warnings - - _buildPortalTemplatesList: (affiliations = []) -> - portalTemplates = [] - for aff in affiliations - if aff.portal && aff.portal.slug && aff.portal.templates_count && aff.portal.templates_count > 0 - portalPath = if aff.institution.isUniversity then '/edu/' else '/org/' - portalTemplates.push({ - name: aff.institution.name - url: Settings.siteUrl + portalPath + aff.portal.slug - }) - return portalTemplates - -defaultSettingsForAnonymousUser = (user_id)-> - id : user_id - ace: - mode:'none' - theme:'textmate' - fontSize: '12' - autoComplete: true - spellCheckLanguage: "" - pdfViewer: "" - syntaxValidation: true - subscription: - freeTrial: - allowed: true - featureSwitches: - github: false - -THEME_LIST = [] -do generateThemeList = () -> - files = fs.readdirSync __dirname + '/../../../../public/js/' + PackageVersions.lib('ace') - for file in files - if file.slice(-2) == "js" and /^theme-/.test(file) - cleanName = file.slice(0,-3).slice(6) - THEME_LIST.push cleanName diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee deleted file mode 100644 index a11e1ba93d..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee +++ /dev/null @@ -1,131 +0,0 @@ -logger = require('logger-sharelatex') -async = require("async") -metrics = require('metrics-sharelatex') -Settings = require('settings-sharelatex') -ObjectId = require('mongoose').Types.ObjectId -Project = require('../../models/Project').Project -Folder = require('../../models/Folder').Folder -ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') -ProjectDetailsHandler = require('./ProjectDetailsHandler') -HistoryManager = require('../History/HistoryManager') -User = require('../../models/User').User -fs = require('fs') -Path = require "path" -_ = require "underscore" -AnalyticsManger = require("../Analytics/AnalyticsManager") - -module.exports = ProjectCreationHandler = - - createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)-> - metrics.inc("project-creation") - if arguments.length == 3 - callback = attributes - attributes = null - - ProjectDetailsHandler.validateProjectName projectName, (error) -> - return callback(error) if error? - logger.log owner_id:owner_id, projectName:projectName, "creating blank project" - if attributes? - ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) -> - return callback(error) if error? - AnalyticsManger.recordEvent( - owner_id, 'project-imported', { projectId: project._id, attributes: attributes } - ) - callback(error, project) - else - HistoryManager.initializeProject (error, history) -> - return callback(error) if error? - attributes = overleaf: history: id: history?.overleaf_id - ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) -> - return callback(error) if error? - AnalyticsManger.recordEvent( - owner_id, 'project-created', { projectId: project._id } - ) - callback(error, project) - - _createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)-> - rootFolder = new Folder {'name':'rootFolder'} - - attributes.owner_ref = new ObjectId(owner_id) - attributes.name = projectName - project = new Project attributes - - Object.assign(project, attributes) - - if Settings.apis?.project_history?.displayHistoryForNewProjects - project.overleaf.history.display = true - if Settings.currentImageName? - # avoid clobbering any imageName already set in attributes (e.g. importedImageName) - project.imageName ?= Settings.currentImageName - project.rootFolder[0] = rootFolder - User.findById owner_id, "ace.spellCheckLanguage", (err, user)-> - if user? # It's possible the owner_id is a UserStub - project.spellCheckLanguage = user.ace.spellCheckLanguage - project.save (err)-> - return callback(err) if err? - callback err, project - - createProjectFromSnippet : (owner_id, projectName, docLines, callback = (error, project) ->)-> - @createBlankProject owner_id, projectName, (error, project)-> - return callback(error) if error? - ProjectCreationHandler._createRootDoc project, owner_id, docLines, callback - - createBasicProject : (owner_id, projectName, callback = (error, project) ->)-> - self = @ - @createBlankProject owner_id, projectName, (error, project)-> - return callback(error) if error? - self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)-> - return callback(error) if error? - ProjectCreationHandler._createRootDoc project, owner_id, docLines, callback - - createExampleProject: (owner_id, projectName, callback = (error, project) ->)-> - self = @ - @createBlankProject owner_id, projectName, (error, project)-> - return callback(error) if error? - async.series [ - (callback) -> - self._buildTemplate "main.tex", owner_id, projectName, (error, docLines)-> - return callback(error) if error? - ProjectCreationHandler._createRootDoc project, owner_id, docLines, callback - (callback) -> - self._buildTemplate "references.bib", owner_id, projectName, (error, docLines)-> - return callback(error) if error? - ProjectEntityUpdateHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, owner_id, (error, doc)-> - callback(error) - (callback) -> - universePath = Path.resolve(__dirname + "/../../../templates/project_files/universe.jpg") - ProjectEntityUpdateHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, null, owner_id, callback - ], (error) -> - callback(error, project) - - _createRootDoc: (project, owner_id, docLines, callback = (error, project) ->)-> - ProjectEntityUpdateHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)-> - if error? - logger.err err:error, "error adding root doc when creating project" - return callback(error) - ProjectEntityUpdateHandler.setRootDoc project._id, doc._id, (error) -> - callback(error, project) - - _buildTemplate: (template_name, user_id, project_name, callback = (error, output) ->)-> - User.findById user_id, "first_name last_name", (error, user)-> - return callback(error) if error? - monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] - - templatePath = Path.resolve(__dirname + "/../../../templates/project_files/#{template_name}") - fs.readFile templatePath, (error, template) -> - return callback(error) if error? - data = - project_name: project_name - user: user - year: new Date().getUTCFullYear() - month: monthNames[new Date().getUTCMonth()] - output = _.template(template.toString(), data) - callback null, output.split("\n") - -metrics.timeAsyncMethod( - ProjectCreationHandler, 'createBlankProject', - 'mongo.ProjectCreationHandler', - logger -) - - diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee deleted file mode 100644 index 3ae188173f..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ /dev/null @@ -1,92 +0,0 @@ -Project = require('../../models/Project').Project -DeletedProject = require('../../models/DeletedProject').DeletedProject -logger = require('logger-sharelatex') -documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') -tagsHandler = require("../Tags/TagsHandler") -async = require("async") -FileStoreHandler = require("../FileStore/FileStoreHandler") -CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") - -module.exports = ProjectDeleter = - - markAsDeletedByExternalSource : (project_id, callback = (error) ->)-> - logger.log project_id:project_id, "marking project as deleted by external data source" - conditions = {_id:project_id} - update = {deletedByExternalDataSource:true} - - Project.update conditions, update, {}, (err)-> - require('../Editor/EditorController').notifyUsersProjectHasBeenDeletedOrRenamed project_id, -> - callback() - - unmarkAsDeletedByExternalSource: (project_id, callback = (error) ->) -> - logger.log project_id: project_id, "removing flag marking project as deleted by external data source" - conditions = {_id:project_id.toString()} - update = {deletedByExternalDataSource: false} - Project.update conditions, update, {}, callback - - deleteUsersProjects: (user_id, callback) -> - logger.log {user_id}, "deleting users projects" - - Project.find {owner_ref: user_id}, (error, projects) -> - return callback(error) if error? - async.each( - projects, - (project, cb) -> - ProjectDeleter.deleteProject project._id, cb - (err) -> - return callback(err) if err? - CollaboratorsHandler.removeUserFromAllProjets user_id, callback - ) - - deleteProject: (project_id, options = {}, callback = (error) ->) -> - data = {} - logger.log project_id: project_id, "deleting project" - - if typeof options == 'function' - callback = options - options = {} - - async.waterfall [ - (cb) -> - Project.findOne {_id: project_id}, (err, project) -> cb(err, project) - (project, cb) -> - deletedProject = new DeletedProject() - deletedProject.project = project - deletedProject.deleterData = - deletedAt: new Date() - deleterId: options.deleterUser?._id - deleterIpAddress: options.ipAddress - - return callback(new Errors.NotFoundError("project not found")) unless project? - - deletedProject.save (err) -> - cb(err, deletedProject) - (deletedProject, cb) -> - documentUpdaterHandler.flushProjectToMongoAndDelete project_id, (err) -> - cb(err, deletedProject) - (deletedProject, cb) -> - CollaboratorsHandler.getMemberIds project_id, (error, member_ids = []) -> - for member_id in member_ids - tagsHandler.removeProjectFromAllTags member_id, project_id, (err)-> - cb(null, deletedProject) #doesn't matter if this fails or the order it happens in - (deletedProject, cb) -> - Project.remove _id: project_id, (err) -> - cb(err, deletedProject) - ], (err, deletedProject) -> - if err? - logger.err err:err, "problem deleting project" - return callback(err) - logger.log project_id:project_id, "successfully deleting project from user request" - callback(null, deletedProject) - - archiveProject: (project_id, callback = (error) ->)-> - logger.log project_id:project_id, "archived project from user request" - Project.update {_id:project_id}, { $set: { archived: true }}, (err)-> - if err? - logger.err err:err, "problem archived project" - return callback(err) - logger.log project_id:project_id, "successfully archived project from user request" - callback() - - restoreProject: (project_id, callback = (error) ->) -> - Project.update {_id:project_id}, { $unset: { archived: true }}, callback diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee deleted file mode 100644 index 172b925425..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee +++ /dev/null @@ -1,184 +0,0 @@ -ProjectGetter = require("./ProjectGetter") -UserGetter = require("../User/UserGetter") -Project = require('../../models/Project').Project -ObjectId = require("mongojs").ObjectId -logger = require("logger-sharelatex") -tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender' -_ = require("underscore") -PublicAccessLevels = require("../Authorization/PublicAccessLevels") -Errors = require("../Errors/Errors") -ProjectTokenGenerator = require('./ProjectTokenGenerator') -ProjectEntityHandler = require('./ProjectEntityHandler') -ProjectHelper = require('./ProjectHelper') -CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') -settings = require('settings-sharelatex') - - -module.exports = ProjectDetailsHandler = - getDetails: (project_id, callback)-> - ProjectGetter.getProject project_id, {name:true, description:true, compiler:true, features:true, owner_ref:true, overleaf:true}, (err, project)-> - if err? - logger.err err:err, project_id:project_id, "error getting project" - return callback(err) - return callback(new Errors.NotFoundError("project not found")) if !project? - UserGetter.getUser project.owner_ref, (err, user) -> - return callback(err) if err? - details = - name : project.name - description: project.description - compiler: project.compiler - features: user?.features or settings.defaultFeatures - - if project.overleaf? - details.overleaf = project.overleaf - - logger.log project_id:project_id, details: details, "getting project details" - callback(err, details) - - getProjectDescription: (project_id, callback)-> - ProjectGetter.getProject project_id, description: true, (err, project)-> - callback(err, project?.description) - - setProjectDescription: (project_id, description, callback)-> - conditions = _id:project_id - update = description:description - logger.log conditions:conditions, update:update, project_id:project_id, description:description, "setting project description" - Project.update conditions, update, (err)-> - if err? - logger.err err:err, "something went wrong setting project description" - callback(err) - - transferOwnership: (project_id, user_id, suffix = "", callback)-> - if typeof suffix is 'function' - callback = suffix - suffix = '' - ProjectGetter.getProject project_id, {owner_ref: true, name: true}, (err, project)-> - return callback(err) if err? - return callback(new Errors.NotFoundError("project not found")) unless project? - return callback() if project.owner_ref == user_id - - UserGetter.getUser user_id, (err, user) -> - return callback(err) if err? - return callback(new Errors.NotFoundError("user not found")) unless user? - - # we make sure the user to which the project is transferred is not a collaborator for the project, - # this prevents any conflict during unique name generation - CollaboratorsHandler.removeUserFromProject project_id, user_id, (err) -> - return callback(err) if err? - ProjectDetailsHandler.generateUniqueName user_id, project.name + suffix, (err, name) -> - return callback(err) if err? - Project.update {_id: project_id}, - { - $set: { - owner_ref: user_id, - name: name - } - }, (err) -> - return callback(err) if err? - ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, callback - - renameProject: (project_id, newName, callback = ->)-> - ProjectDetailsHandler.validateProjectName newName, (error) -> - return callback(error) if error? - logger.log project_id: project_id, newName:newName, "renaming project" - ProjectGetter.getProject project_id, {name:true}, (err, project)-> - if err? or !project? - logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename" - return callback(err) - oldProjectName = project.name - Project.update _id:project_id, {name: newName}, (err, project)=> - if err? - return callback(err) - tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback - - MAX_PROJECT_NAME_LENGTH: 150 - validateProjectName: (name, callback = (error) ->) -> - if !name? or name.length == 0 - return callback(new Errors.InvalidNameError("Project name cannot be blank")) - else if name.length > @MAX_PROJECT_NAME_LENGTH - return callback(new Errors.InvalidNameError("Project name is too long")) - else if name.indexOf("/") > -1 - return callback(new Errors.InvalidNameError("Project name cannot contain / characters")) - else if name.indexOf("\\") > -1 - return callback(new Errors.InvalidNameError("Project name cannot contain \\ characters")) - else - return callback() - - generateUniqueName: (user_id, name, suffixes = [], callback = (error, newName) -> ) -> - if arguments.length is 3 && typeof suffixes is 'function' # make suffixes an optional argument - callback = suffixes - suffixes = [] - ProjectDetailsHandler.ensureProjectNameIsUnique user_id, name, suffixes, callback - - # FIXME: we should put a lock around this to make it completely safe, but we would need to do that at - # the point of project creation, rather than just checking the name at the start of the import. - # If we later move this check into ProjectCreationHandler we can ensure all new projects are created - # with a unique name. But that requires thinking through how we would handle incoming projects from - # dropbox for example. - ensureProjectNameIsUnique: (user_id, name, suffixes = [], callback = (error, name, changed)->) -> - ProjectGetter.findAllUsersProjects user_id, {name: 1}, (error, allUsersProjectNames) -> - return callback(error) if error? - # allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]} - # collect all of the names and flatten them into a single array - projectNameList = _.pluck(_.flatten(_.values(allUsersProjectNames)),'name') - ProjectHelper.ensureNameIsUnique projectNameList, name, suffixes, ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH, callback - - fixProjectName: (name) -> - if name == "" || !name - name = "Untitled" - if name.indexOf('/') > -1 - # v2 does not allow / in a project name - name = name.replace(/\//g, '-') - if name.indexOf('\\') > -1 - # backslashes in project name will prevent syncing to dropbox - name = name.replace(/\\/g, '') - if name.length > @MAX_PROJECT_NAME_LENGTH - name = name.substr(0, @MAX_PROJECT_NAME_LENGTH) - return name - - setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)-> - logger.log project_id: project_id, level: newAccessLevel, "set public access level" - # DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer - # be passed here. Remove after token-based access has been live for a while - if project_id? && newAccessLevel? and _.include [ - PublicAccessLevels.READ_ONLY, - PublicAccessLevels.READ_AND_WRITE, - PublicAccessLevels.PRIVATE, - PublicAccessLevels.TOKEN_BASED - ], newAccessLevel - Project.update {_id:project_id},{publicAccesLevel:newAccessLevel}, (err)-> - callback(err) - - ensureTokensArePresent: (project_id, callback=(err, tokens)->) -> - ProjectGetter.getProject project_id, {tokens: 1}, (err, project) -> - return callback(err) if err? - if project.tokens? and project.tokens.readOnly? and project.tokens.readAndWrite? - logger.log {project_id}, "project already has tokens" - return callback(null, project.tokens) - else - logger.log { - project_id, - has_tokens: project.tokens?, - has_readOnly: project?.tokens?.readOnly?, - has_readAndWrite: project?.tokens?.readAndWrite? - }, "generating tokens for project" - ProjectDetailsHandler._generateTokens project, (err) -> - return callback(err) if err? - Project.update {_id: project_id}, {$set: {tokens: project.tokens}}, (err) -> - return callback(err) if err? - callback(null, project.tokens) - - _generateTokens: (project, callback=(err)->) -> - project.tokens ||= {} - tokens = project.tokens - if !tokens.readAndWrite? - { token, numericPrefix } = ProjectTokenGenerator.readAndWriteToken() - tokens.readAndWrite = token - tokens.readAndWritePrefix = numericPrefix - if !tokens.readOnly? - ProjectTokenGenerator.generateUniqueReadOnlyToken (err, token) -> - return callback(err) if err? - tokens.readOnly = token - callback() - else - callback() diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee deleted file mode 100644 index d461ed4699..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee +++ /dev/null @@ -1,125 +0,0 @@ -projectCreationHandler = require('./ProjectCreationHandler') -ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') -projectLocator = require('./ProjectLocator') -projectOptionsHandler = require('./ProjectOptionsHandler') -projectDeleter = require('./ProjectDeleter') -DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler") -DocstoreManager = require "../Docstore/DocstoreManager" -ProjectGetter = require("./ProjectGetter") -_ = require('underscore') -async = require('async') -logger = require("logger-sharelatex") - - -module.exports = ProjectDuplicator = - - _copyDocs: (owner_id, newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)-> - setRootDoc = _.once (doc_id)-> - ProjectEntityUpdateHandler.setRootDoc newProject._id, doc_id - docs = originalFolder.docs or [] - jobs = docs.map (doc)-> - return (cb)-> - if !doc?._id? - return callback() - content = docContents[doc._id.toString()] - ProjectEntityUpdateHandler.addDoc newProject._id, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)-> - if err? - logger.err err:err, "error copying doc" - return callback(err) - if originalRootDoc? and newDoc.name == originalRootDoc.name - setRootDoc newDoc._id - cb() - - async.series jobs, callback - - _copyFiles: (owner_id, newProject, originalProject_id, originalFolder, desFolder, callback)-> - fileRefs = originalFolder.fileRefs or [] - firstError = null # track first error to exit gracefully from parallel copy - jobs = fileRefs.map (file)-> - return (cb)-> - return async.setImmediate(cb) if firstError? # skip further copies if an error has occurred - ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject newProject._id, newProject, desFolder._id, originalProject_id, file, owner_id, (err) -> - firstError ||= err if err? # set the error flag if this copy failed - return cb() - # If one of these jobs fails then we wait until all running jobs have - # finished, skipping those which have not started yet. We need to wait - # for all the copy jobs to finish to avoid them writing to the project - # entry in the background while we are deleting it. - async.parallelLimit jobs, 5, (err) -> - return callback(firstError) if firstError? - return callback(err) if err? # shouldn't happen - return callback() - - - _copyFolderRecursivly: (owner_id, newProject_id, originalProject_id, originalRootDoc, originalFolder, desFolder, docContents, callback)-> - ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)-> - if err? - logger.err project_id:newProject_id, "could not get project" - return callback(err) - - folders = originalFolder.folders or [] - - jobs = folders.map (childFolder)-> - return (cb)-> - if !childFolder?._id? - return cb() - ProjectEntityUpdateHandler.addFolder newProject._id, desFolder?._id, childFolder.name, (err, newFolder)-> - return cb(err) if err? - ProjectDuplicator._copyFolderRecursivly owner_id, newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb - - jobs.push (cb)-> - ProjectDuplicator._copyFiles owner_id, newProject, originalProject_id, originalFolder, desFolder, cb - jobs.push (cb)-> - ProjectDuplicator._copyDocs owner_id, newProject, originalRootDoc, originalFolder, desFolder, docContents, cb - - async.series jobs, callback - - duplicate: (owner, originalProject_id, newProjectName, callback)-> - - jobs = - flush: (cb)-> - DocumentUpdaterHandler.flushProjectToMongo originalProject_id, cb - originalProject: (cb)-> - ProjectGetter.getProject originalProject_id, {compiler:true, rootFolder:true, rootDoc_id:true}, cb - originalRootDoc: (cb)-> - projectLocator.findRootDoc {project_id:originalProject_id}, cb - docContentsArray: (cb)-> - DocstoreManager.getAllDocs originalProject_id, cb - - # Get the contents of the original project first - async.series jobs, (err, results)-> - if err? - logger.err err:err, originalProject_id:originalProject_id, "error duplicating project reading original project" - return callback(err) - {originalProject, originalRootDoc, docContentsArray} = results - - originalRootDoc = originalRootDoc?[0] - - docContents = {} - for docContent in docContentsArray - docContents[docContent._id] = docContent - - # Now create the new project, cleaning it up on failure if necessary - projectCreationHandler.createBlankProject owner._id, newProjectName, (err, newProject) -> - if err? - logger.err err:err, originalProject_id:originalProject_id, "error duplicating project when creating new project" - return callback(err) - - copyJobs = - setCompiler: (cb) -> - projectOptionsHandler.setCompiler newProject._id, originalProject.compiler, cb - copyFiles: (cb) -> - ProjectDuplicator._copyFolderRecursivly owner._id, newProject._id, originalProject_id, originalRootDoc, originalProject.rootFolder[0], newProject.rootFolder[0], docContents, cb - - # Copy the contents of the original project into the new project - async.series copyJobs, (err) -> - if err? - logger.err err:err, originalProject_id:originalProject_id, newProjectName:newProjectName, newProject_id: newProject._id, "error cloning project, will delete broken clone" - # Clean up broken clone on error. - # Make sure we delete the new failed project, not the original one! - projectDeleter.deleteProject newProject._id, (delete_err) -> - if delete_err? - logger.error newProject_id: newProject._id, delete_err:delete_err, "error deleting broken clone of project" - callback(err) - else - callback(null, newProject) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee deleted file mode 100644 index f81c59c399..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ /dev/null @@ -1,93 +0,0 @@ -_ = require("underscore") -Path = require 'path' - -module.exports = ProjectEditorHandler = - trackChangesAvailable: false - - buildProjectModelView: (project, members, invites) -> - result = - _id : project._id - name : project.name - rootDoc_id : project.rootDoc_id - rootFolder : [@buildFolderModelView project.rootFolder[0]] - publicAccesLevel : project.publicAccesLevel - dropboxEnabled : !!project.existsInDropbox - compiler : project.compiler - description: project.description - spellCheckLanguage: project.spellCheckLanguage - deletedByExternalDataSource : project.deletedByExternalDataSource || false - deletedDocs: project.deletedDocs - members: [] - invites: invites - tokens: project.tokens - imageName: if project.imageName? then Path.basename(project.imageName) else undefined - - if !result.invites? - result.invites = [] - - {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members) - result.owner = owner - result.members = members - - result.features = _.defaults(ownerFeatures or {}, { - collaborators: -1 # Infinite - versioning: false - dropbox:false - compileTimeout: 60 - compileGroup:"standard" - templates: false - references: false - referencesSearch: false - mendeley: false - trackChanges: false - trackChangesVisible: ProjectEditorHandler.trackChangesAvailable - }) - - # Originally these two feature flags were both signalled by the now-deprecated `references` flag. - # For older users, the presence of the `references` feature flag should still turn on these features. - result.features.referencesSearch = result.features.referencesSearch or result.features.references - result.features.mendeley = result.features.mendeley or result.features.references - - return result - - buildOwnerAndMembersViews: (members) -> - owner = null - ownerFeatures = null - filteredMembers = [] - for member in (members || []) - if member.privilegeLevel == "owner" - ownerFeatures = member.user.features - owner = @buildUserModelView member.user, "owner" - else - filteredMembers.push @buildUserModelView member.user, member.privilegeLevel - return { - owner: owner, - ownerFeatures: ownerFeatures, - members: filteredMembers, - } - - buildUserModelView: (user, privileges) -> - _id : user._id - first_name : user.first_name - last_name : user.last_name - email : user.email - privileges : privileges - signUpDate : user.signUpDate - - buildFolderModelView: (folder) -> - fileRefs = _.filter (folder.fileRefs or []), (file)-> file? - _id : folder._id - name : folder.name - folders : @buildFolderModelView childFolder for childFolder in (folder.folders or []) - fileRefs : @buildFileModelView file for file in fileRefs - docs : @buildDocModelView doc for doc in (folder.docs or []) - - buildFileModelView: (file) -> - _id : file._id - name : file.name - linkedFileData: file.linkedFileData - created: file.created - - buildDocModelView: (doc) -> - _id : doc._id - name : doc.name diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee deleted file mode 100644 index 55ce199312..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ /dev/null @@ -1,136 +0,0 @@ -_ = require('underscore') -async = require "async" -path = require "path" -logger = require('logger-sharelatex') -DocstoreManager = require "../Docstore/DocstoreManager" -DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') -Errors = require '../Errors/Errors' -Project = require('../../models/Project').Project -ProjectGetter = require "./ProjectGetter" -TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') - -module.exports = ProjectEntityHandler = self = - getAllDocs: (project_id, callback) -> - logger.log project_id:project_id, "getting all docs for project" - - # We get the path and name info from the project, and the lines and - # version info from the doc store. - DocstoreManager.getAllDocs project_id, (error, docContentsArray) -> - return callback(error) if error? - - # Turn array from docstore into a dictionary based on doc id - docContents = {} - for docContent in docContentsArray - docContents[docContent._id] = docContent - - self._getAllFolders project_id, (error, folders = {}) -> - return callback(error) if error? - docs = {} - for folderPath, folder of folders - for doc in (folder.docs or []) - content = docContents[doc._id.toString()] - if content? - docs[path.join(folderPath, doc.name)] = { - _id: doc._id - name: doc.name - lines: content.lines - rev: content.rev - } - logger.log count:_.keys(docs).length, project_id:project_id, "returning docs for project" - callback null, docs - - getAllFiles: (project_id, callback) -> - logger.log project_id:project_id, "getting all files for project" - self._getAllFolders project_id, (err, folders = {}) -> - return callback(err) if err? - files = {} - for folderPath, folder of folders - for file in (folder.fileRefs or []) - if file? - files[path.join(folderPath, file.name)] = file - callback null, files - - getAllEntities: (project_id, callback) -> - ProjectGetter.getProject project_id, (err, project) -> - return callback(err) if err? - self.getAllEntitiesFromProject project, callback - - getAllEntitiesFromProject: (project, callback) -> - logger.log project:project, "getting all entities for project" - self._getAllFoldersFromProject project, (err, folders = {}) -> - return callback(err) if err? - docs = [] - files = [] - for folderPath, folder of folders - for doc in (folder.docs or []) - if doc? - docs.push({path: path.join(folderPath, doc.name), doc:doc}) - for file in (folder.fileRefs or []) - if file? - files.push({path: path.join(folderPath, file.name), file:file}) - callback null, docs, files - - getAllDocPathsFromProjectById: (project_id, callback) -> - ProjectGetter.getProjectWithoutDocLines project_id, (err, project) -> - return callback(err) if err? - return callback(Errors.NotFoundError("no project")) if !project? - self.getAllDocPathsFromProject project, callback - - getAllDocPathsFromProject: (project, callback) -> - logger.log project:project, "getting all docs for project" - self._getAllFoldersFromProject project, (err, folders = {}) -> - return callback(err) if err? - docPath = {} - for folderPath, folder of folders - for doc in (folder.docs or []) - docPath[doc._id] = path.join(folderPath, doc.name) - logger.log count:_.keys(docPath).length, project_id:project._id, "returning docPaths for project" - callback null, docPath - - flushProjectToThirdPartyDataStore: (project_id, callback) -> - logger.log project_id:project_id, "flushing project to tpds" - DocumentUpdaterHandler.flushProjectToMongo project_id, (error) -> - return callback(error) if error? - ProjectGetter.getProject project_id, {name:true}, (error, project) -> - return callback(error) if error? - requests = [] - self.getAllDocs project_id, (error, docs) -> - return callback(error) if error? - for docPath, doc of docs - do (docPath, doc) -> - requests.push (cb) -> - TpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0}, cb - self.getAllFiles project_id, (error, files) -> - return callback(error) if error? - for filePath, file of files - do (filePath, file) -> - requests.push (cb) -> - TpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, cb - async.series requests, (err) -> - logger.log project_id:project_id, "finished flushing project to tpds" - callback(err) - - getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev) ->) -> - if typeof(options) == "function" - callback = options - options = {} - - DocstoreManager.getDoc project_id, doc_id, options, callback - - _getAllFolders: (project_id, callback) -> - logger.log project_id:project_id, "getting all folders for project" - ProjectGetter.getProjectWithoutDocLines project_id, (err, project) -> - return callback(err) if err? - return callback(Errors.NotFoundError("no project")) if !project? - self._getAllFoldersFromProject project, callback - - _getAllFoldersFromProject: (project, callback) -> - folders = {} - processFolder = (basePath, folder) -> - folders[basePath] = folder - for childFolder in (folder.folders or []) - if childFolder.name? - processFolder path.join(basePath, childFolder.name), childFolder - - processFolder "/", project.rootFolder[0] - callback null, folders diff --git a/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee deleted file mode 100644 index 74b52f5892..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee +++ /dev/null @@ -1,386 +0,0 @@ -_ = require('underscore') -async = require 'async' -logger = require('logger-sharelatex') -path = require('path') -settings = require('settings-sharelatex') -CooldownManager = require '../Cooldown/CooldownManager' -Errors = require '../Errors/Errors' -Folder = require('../../models/Folder').Folder -LockManager = require('../../infrastructure/LockManager') -Project = require('../../models/Project').Project -ProjectEntityHandler = require('./ProjectEntityHandler') -ProjectGetter = require('./ProjectGetter') -ProjectLocator = require('./ProjectLocator') -SafePath = require './SafePath' - -LOCK_NAMESPACE = "mongoTransaction" - -wrapWithLock = (methodWithoutLock) -> - # This lock is used whenever we read or write to an existing project's - # structure. Some operations to project structure cannot be done atomically - # in mongo, this lock is used to prevent reading the structure between two - # parts of a staged update. - methodWithLock = (project_id, args..., callback) -> - LockManager.runWithLock LOCK_NAMESPACE, project_id, - (cb) -> methodWithoutLock project_id, args..., cb - callback - methodWithLock.withoutLock = methodWithoutLock - methodWithLock - -module.exports = ProjectEntityMongoUpdateHandler = self = - LOCK_NAMESPACE: LOCK_NAMESPACE - - addDoc: wrapWithLock (project_id, folder_id, doc, callback = (err, result) ->) -> - ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) -> - if err? - logger.err project_id:project_id, err:err, "error getting project for add doc" - return callback(err) - logger.log project_id: project_id, folder_id: folder_id, doc_name: doc.name, "adding doc to project with project" - self._confirmFolder project, folder_id, (folder_id) => - self._putElement project, folder_id, doc, "doc", callback - - addFile: wrapWithLock (project_id, folder_id, fileRef, callback = (error, result, project) ->)-> - ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) -> - if err? - logger.err project_id:project_id, err:err, "error getting project for add file" - return callback(err) - logger.log project_id: project._id, folder_id: folder_id, file_name: fileRef.name, "adding file" - self._confirmFolder project, folder_id, (folder_id)-> - self._putElement project, folder_id, fileRef, "file", callback - - replaceFileWithNew: wrapWithLock (project_id, file_id, newFileRef, callback) -> - ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) -> - return callback(err) if err? - ProjectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=> - return callback(err) if err? - ProjectEntityMongoUpdateHandler._insertDeletedFileReference project_id, fileRef, (err) -> - return callback(err) if err? - conditions = _id:project._id - inc = {} - # increment the project structure version as we are adding a new file here - inc['version'] = 1 - set = {} - set["#{path.mongo}._id"] = newFileRef._id - set["#{path.mongo}.created"] = new Date() - set["#{path.mongo}.linkedFileData"] = newFileRef.linkedFileData - inc["#{path.mongo}.rev"] = 1 - set["#{path.mongo}.hash"] = newFileRef.hash - update = - "$inc": inc - "$set": set - # Note: Mongoose uses new:true to return the modified document - # https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate - # but Mongo uses returnNewDocument:true instead - # https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/ - # We are using Mongoose here, but if we ever switch to a direct mongo call - # the next line will need to be updated. - Project.findOneAndUpdate conditions, update, {new:true}, (err, newProject) -> - return callback(err) if err? - callback null, fileRef, project, path, newProject - - mkdirp: wrapWithLock (project_id, path, options, callback) -> - # defaults to case insensitive paths, use options {exactCaseMatch:true} - # to make matching case-sensitive - folders = path.split('/') - folders = _.select folders, (folder)-> - return folder.length != 0 - - ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=> - if path == '/' - logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder" - return callback(null, [], project.rootFolder[0]) - logger.log project_id: project._id, path:path, folders:folders, "running mkdirp" - - builtUpPath = '' - procesFolder = (previousFolders, folderName, callback)=> - previousFolders = previousFolders || [] - parentFolder = previousFolders[previousFolders.length-1] - if parentFolder? - parentFolder_id = parentFolder._id - builtUpPath = "#{builtUpPath}/#{folderName}" - ProjectLocator.findElementByPath project: project, path: builtUpPath, exactCaseMatch: options?.exactCaseMatch, (err, foundFolder)=> - if !foundFolder? - logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" - self.addFolder.withoutLock project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> - return callback(err) if err? - newFolder.parentFolder_id = parentFolder_id - previousFolders.push newFolder - callback null, previousFolders - else - foundFolder.filterOut = true - previousFolders.push foundFolder - callback null, previousFolders - - async.reduce folders, [], procesFolder, (err, folders) -> - return callback(err) if err? - lastFolder = folders[folders.length-1] - folders = _.select folders, (folder)-> - !folder.filterOut - callback null, folders, lastFolder - - moveEntity: wrapWithLock (project_id, entity_id, destFolderId, entityType, callback = (error) ->) -> - ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) -> - return callback(err) if err? - ProjectLocator.findElement {project, element_id: entity_id, type: entityType}, (err, entity, entityPath)-> - return callback(err) if err? - # Prevent top-level docs/files with reserved names (to match v1 behaviour) - if self._blockedFilename entityPath, entityType - return callback new Errors.InvalidNameError("blocked element name") - self._checkValidMove project, entityType, entity, entityPath, destFolderId, (error) -> - return callback(error) if error? - ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) -> - return callback(error) if error? - # For safety, insert the entity in the destination - # location first, and then remove the original. If - # there is an error the entity may appear twice. This - # will cause some breakage but is better than being - # lost, which is what happens if this is done in the - # opposite order. - self._putElement project, destFolderId, entity, entityType, (err, result)-> - return callback(err) if err? - # Note: putElement always pushes onto the end of an - # array so it will never change an existing mongo - # path. Therefore it is safe to remove an element - # from the project with an existing path after - # calling putElement. But we must be sure that we - # have not moved a folder subfolder of itself (which - # is done by _checkValidMove above) because that - # would lead to it being deleted. - self._removeElementFromMongoArray Project, project_id, entityPath.mongo, entity_id, (err, newProject)-> - return callback(err) if err? - ProjectEntityHandler.getAllEntitiesFromProject newProject, (err, newDocs, newFiles) -> - return callback(err) if err? - startPath = entityPath.fileSystem - endPath = result.path.fileSystem - changes = {oldDocs, newDocs, oldFiles, newFiles, newProject} - # check that no files have been lost (or duplicated) - if (oldFiles.length != newFiles.length) or (oldDocs.length != newDocs.length) - logger.err { - project_id: project_id - oldDocs: oldDocs.length - newDocs: newDocs.length - oldFiles:oldFiles.length - newFiles: newFiles.length - origProject: project - newProject: newProject - }, "project corrupted moving files - shouldn't happen" - return callback(new Error("unexpected change in project structure")) - callback null, project, startPath, endPath, entity.rev, changes, callback - - deleteEntity: wrapWithLock (project_id, entity_id, entityType, callback) -> - ProjectGetter.getProjectWithoutLock project_id, {name:true, rootFolder:true, overleaf:true}, (error, project) -> - return callback(error) if error? - ProjectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path) -> - return callback(error) if error? - self._removeElementFromMongoArray Project, project_id, path.mongo, entity_id, (error, newProject) -> - return callback(error) if error? - callback null, entity, path, project, newProject - - renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, callback) -> - ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (error, project)=> - return callback(error) if error? - ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) => - return callback(error) if error? - ProjectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (error, entity, entPath, parentFolder)=> - return callback(error) if error? - endPath = path.join(path.dirname(entPath.fileSystem), newName) - # Prevent top-level docs/files with reserved names (to match v1 behaviour) - if self._blockedFilename {fileSystem: endPath}, entityType - return callback new Errors.InvalidNameError("blocked element name") - # check if the new name already exists in the current folder - self._checkValidElementName parentFolder, newName, (error) => - return callback(error) if error? - conditions = {_id:project_id} - update = "$set":{}, "$inc":{} - namePath = entPath.mongo+".name" - update["$set"][namePath] = newName - # we need to increment the project version number for any structure change - update["$inc"]["version"] = 1 - Project.findOneAndUpdate conditions, update, { "new": true}, (error, newProject) -> - return callback(error) if error? - ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => - return callback(error) if error? - startPath = entPath.fileSystem - changes = {oldDocs, newDocs, oldFiles, newFiles, newProject} - callback null, project, startPath, endPath, entity.rev, changes, callback - - addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) -> - ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) -> - if err? - logger.err project_id:project_id, err:err, "error getting project for add folder" - return callback(err) - self._confirmFolder project, parentFolder_id, (parentFolder_id) => - folder = new Folder name: folderName - logger.log project: project._id, parentFolder_id:parentFolder_id, folderName:folderName, "adding new folder" - self._putElement project, parentFolder_id, folder, "folder", (err)=> - if err? - logger.err err:err, project_id:project._id, "error adding folder to project" - return callback(err) - callback null, folder, parentFolder_id - - _removeElementFromMongoArray: (model, model_id, path, element_id, callback = (err, project) ->)-> - conditions = {_id:model_id} - pullUpdate = {"$pull":{}, "$inc":{}} - nonArrayPath = path.slice(0, path.lastIndexOf(".")) - # remove specific element from array by id - pullUpdate["$pull"][nonArrayPath] = {_id: element_id} - # we need to increment the project version number for any structure change - pullUpdate["$inc"]["version"] = 1 - model.findOneAndUpdate conditions, pullUpdate, {"new": true}, callback - - _countElements: (project)-> - countFolder = (folder)-> - total = 0 - - for subfolder in folder?.folders or [] - total += countFolder(subfolder) - - if folder?.folders?.length? - total += folder.folders.length - - if folder?.docs?.length? - total += folder.docs.length - - if folder?.fileRefs?.length? - total += folder.fileRefs.length - - total - - countFolder project.rootFolder[0] - - _putElement: (project, folder_id, element, type, callback = (err, path, project)->)-> - sanitizeTypeOfElement = (elementType)-> - lastChar = elementType.slice -1 - if lastChar != "s" - elementType +="s" - if elementType == "files" - elementType = "fileRefs" - return elementType - - if !element? or !element._id? - e = new Error("no element passed to be inserted") - logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null" - return callback(e) - type = sanitizeTypeOfElement type - - # original check path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/") - # check if name is allowed - if not SafePath.isCleanFilename element.name - e = new Errors.InvalidNameError("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 - - if self._countElements(project) > settings.maxEntitiesPerProject - logger.warn project_id:project._id, "project too big, stopping insertions" - CooldownManager.putProjectOnCooldown(project._id) - return callback("project_has_to_many_files") - - ProjectLocator.findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=> - if err? - logger.err err:err, project_id:project._id, folder_id:folder_id, type:type, element:element, "error finding folder for _putElement" - return callback(err) - newPath = - fileSystem: "#{path.fileSystem}/#{element.name}" - mongo: path.mongo - # check if the path would be too long - if not SafePath.isAllowedLength newPath.fileSystem - return callback new Errors.InvalidNameError("path too long") - # Prevent top-level docs/files with reserved names (to match v1 behaviour) - if self._blockedFilename newPath, type - return callback new Errors.InvalidNameError("blocked element name") - self._checkValidElementName folder, element.name, (err) => - return callback(err) if err? - id = element._id+'' - element._id = require('mongoose').Types.ObjectId(id) - conditions = _id:project._id - mongopath = "#{path.mongo}.#{type}" - update = "$push":{}, "$inc":{} - update["$push"][mongopath] = element - # we need to increment the project version number for any structure change - update["$inc"]["version"] = 1 # increment project version number - logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project" - # We are using Mongoose here, but if we ever switch to a direct mongo call - # the next line will need to be updated to {returnNewDocument:true} - Project.findOneAndUpdate conditions, update, {"new": true}, (err, newProject)-> - if err? - logger.err err: err, project_id: project._id, 'error saving in putElement project' - return callback(err) - callback(err, {path:newPath}, newProject) - - _blockedFilename: (entityPath, entityType) -> - # check if name would be blocked in v1 - # javascript reserved names are forbidden for docs and files - # at the top-level (but folders with reserved names are allowed). - isFolder = (entityType in ['folder', 'folders']) - [dir, file] = [path.dirname(entityPath.fileSystem), path.basename(entityPath.fileSystem)] - isTopLevel = dir is '/' - if isTopLevel and !isFolder && SafePath.isBlockedFilename file - return true - else - return false - - _checkValidElementName: (folder, name, callback = (err) ->) -> - # check if the name is already taken by a doc, file or - # folder. If so, return an error "file already exists". - err = new Errors.InvalidNameError("file already exists") - for doc in folder?.docs or [] - return callback(err) if doc.name is name - for file in folder?.fileRefs or [] - return callback(err) if file.name is name - for folder in folder?.folders or [] - return callback(err) if folder.name is name - callback() - - _confirmFolder: (project, folder_id, callback)-> - logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project" - if folder_id+'' == 'undefined' - callback(project.rootFolder[0]._id) - else if folder_id != null - callback folder_id - else - callback(project.rootFolder[0]._id) - - _checkValidMove: (project, entityType, entity, entityPath, destFolderId, callback = (error) ->) -> - ProjectLocator.findElement { project, element_id: destFolderId, type:"folder"}, (err, destEntity, destFolderPath) -> - return callback(err) if err? - # check if there is already a doc/file/folder with the same name - # in the destination folder - self._checkValidElementName destEntity, entity.name, (err)-> - return callback(err) if err? - if /folder/.test(entityType) - logger.log destFolderPath: destFolderPath.fileSystem, folderPath: entityPath.fileSystem, "checking folder is not moving into child folder" - isNestedFolder = destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) == entityPath.fileSystem - if isNestedFolder - return callback(new Errors.InvalidNameError("destination folder is a child folder of me")) - callback() - - _insertDeletedDocReference: (project_id, doc, callback = (error) ->) -> - Project.update { - _id: project_id - }, { - $push: { - deletedDocs: { - _id: doc._id - name: doc.name - deletedAt: new Date() - } - } - }, {}, callback - - _insertDeletedFileReference: (project_id, fileRef, callback = (error) ->) -> - Project.update { - _id: project_id - }, { - $push: { - deletedFiles: { - _id: fileRef._id - name: fileRef.name - linkedFileData: fileRef.linkedFileData - hash: fileRef.hash - deletedAt: new Date() - } - } - }, {}, callback diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee deleted file mode 100644 index ba405b0b27..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ /dev/null @@ -1,500 +0,0 @@ -_ = require 'lodash' -async = require 'async' -logger = require('logger-sharelatex') -path = require('path') -Doc = require('../../models/Doc').Doc -DocstoreManager = require('../Docstore/DocstoreManager') -DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') -Errors = require '../Errors/Errors' -File = require('../../models/File').File -FileStoreHandler = require('../FileStore/FileStoreHandler') -LockManager = require('../../infrastructure/LockManager') -Project = require('../../models/Project').Project -ProjectEntityHandler = require('./ProjectEntityHandler') -ProjectGetter = require('./ProjectGetter') -ProjectLocator = require('./ProjectLocator') -ProjectUpdateHandler = require('./ProjectUpdateHandler') -ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') -SafePath = require './SafePath' -TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') - -LOCK_NAMESPACE = "sequentialProjectStructureUpdateLock" - -wrapWithLock = (methodWithoutLock) -> - # This lock is used to make sure that the project structure updates are made - # sequentially. In particular the updates must be made in mongo and sent to - # the doc-updater in the same order. - if typeof methodWithoutLock is 'function' - methodWithLock = (project_id, args..., callback) -> - LockManager.runWithLock LOCK_NAMESPACE, project_id, - (cb) -> methodWithoutLock project_id, args..., cb - callback - methodWithLock.withoutLock = methodWithoutLock - methodWithLock - else - # handle case with separate setup and locked stages - wrapWithSetup = methodWithoutLock.beforeLock # a function to set things up before the lock - mainTask = methodWithoutLock.withLock # function to execute inside the lock - methodWithLock = wrapWithSetup (project_id, args..., callback) -> - LockManager.runWithLock(LOCK_NAMESPACE, project_id, (cb) -> - mainTask(project_id, args..., cb) - callback) - methodWithLock.withoutLock = wrapWithSetup mainTask - methodWithLock.beforeLock = methodWithoutLock.beforeLock - methodWithLock.mainTask = methodWithoutLock.withLock - methodWithLock - -module.exports = ProjectEntityUpdateHandler = self = - copyFileFromExistingProjectWithProject: wrapWithLock - beforeLock: (next) -> - (project_id, project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)-> - logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project" - ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id) -> - if !origonalFileRef? - logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null" - return callback() - # convert any invalid characters in original file to '_' - fileProperties = name : SafePath.clean(origonalFileRef.name) - if origonalFileRef.linkedFileData? - fileProperties.linkedFileData = origonalFileRef.linkedFileData - if origonalFileRef.hash? - fileProperties.hash = origonalFileRef.hash - fileRef = new File(fileProperties) - FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)-> - if err? - logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3" - return callback(err) - next(project_id, project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback) - withLock: (project_id, project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)-> - projectHistoryId = project.overleaf?.history?.id - ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result, newProject) -> - if err? - logger.err { err, project_id, folder_id }, "error putting element as part of copy" - return callback(err) - TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) -> - if err? - logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker" - newFiles = [ - file: fileRef - path: result?.path?.fileSystem - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject}, (error) -> - return callback(error) if error? - callback null, fileRef, folder_id - - updateDocLines: (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->)-> - ProjectGetter.getProjectWithoutDocLines project_id, (err, project)-> - return callback(err) if err? - return callback(new Errors.NotFoundError("project not found")) if !project? - logger.log project_id: project_id, doc_id: doc_id, "updating doc lines" - ProjectLocator.findElement {project:project, element_id:doc_id, type:"docs"}, (err, doc, path)-> - isDeletedDoc = false - if err? - if err instanceof Errors.NotFoundError - # We need to be able to update the doclines of deleted docs. This is - # so the doc-updater can flush a doc's content to the doc-store after - # the doc is deleted. - isDeletedDoc = true - doc = _.find project.deletedDocs, (doc) -> - doc._id.toString() == doc_id.toString() - else - return callback(err) - - if !doc? - # Do not allow an update to a doc which has never exist on this project - logger.error {doc_id, project_id, lines}, "doc not found while updating doc lines" - return callback(new Errors.NotFoundError('doc not found')) - - logger.log {project_id, doc_id}, "telling docstore manager to update doc" - DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) -> - if err? - logger.error {err, doc_id, project_id, lines}, "error sending doc to docstore" - return callback(err) - logger.log {project_id, doc_id, modified}, "finished updating doc lines" - # path will only be present if the doc is not deleted - if modified && !isDeletedDoc - # Don't need to block for marking as updated - ProjectUpdateHandler.markAsUpdated project_id, lastUpdatedAt, lastUpdatedBy - TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback - else - callback() - - setRootDoc: (project_id, newRootDocID, callback = (error) ->)-> - logger.log project_id: project_id, rootDocId: newRootDocID, "setting root doc" - Project.update {_id:project_id}, {rootDoc_id:newRootDocID}, {}, callback - - unsetRootDoc: (project_id, callback = (error) ->) -> - logger.log project_id: project_id, "removing root doc" - Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback - - _addDocAndSendToTpds: (project_id, folder_id, doc, callback = (error, result, project) ->)-> - ProjectEntityMongoUpdateHandler.addDoc project_id, folder_id, doc, (err, result, project) -> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, doc_name: doc?.name, doc_id:doc?._id, "error adding file with project" - return callback(err) - TpdsUpdateSender.addDoc { - project_id: project_id, - doc_id: doc?._id, - path: result?.path?.fileSystem, - project_name: project.name, - rev: 0 - }, (err) -> - return callback(err) if err? - callback(null, result, project) - - addDoc: (project_id, folder_id, docName, docLines, userId, callback) -> - self.addDocWithRanges(project_id, folder_id, docName, docLines, {}, userId, callback) - - addDocWithRanges: wrapWithLock - beforeLock: (next) -> - (project_id, folder_id, docName, docLines, ranges, userId, callback = (error, doc, folder_id) ->) -> - if not SafePath.isCleanFilename docName - return callback new Errors.InvalidNameError("invalid element name") - # Put doc in docstore first, so that if it errors, we don't have a doc_id in the project - # which hasn't been created in docstore. - doc = new Doc name: docName - DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, ranges, (err, modified, rev) -> - return callback(err) if err? - next(project_id, folder_id, doc, docName, docLines, ranges, userId, callback) - withLock: (project_id, folder_id, doc, docName, docLines, ranges, userId, callback = (error, doc, folder_id) ->) -> - ProjectEntityUpdateHandler._addDocAndSendToTpds project_id, folder_id, doc, (err, result, project) -> - return callback(err) if err? - docPath = result?.path?.fileSystem - projectHistoryId = project.overleaf?.history?.id - newDocs = [ - doc: doc - path: docPath - docLines: docLines.join('\n') - ] - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newDocs, newProject: project}, (error) -> - return callback(error) if error? - callback null, doc, folder_id - - _uploadFile: (project_id, folder_id, fileName, fsPath, linkedFileData, callback = (error, fileStoreUrl, fileRef) ->)-> - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") - fileArgs = - name: fileName - linkedFileData: linkedFileData - FileStoreHandler.uploadFileFromDisk project_id, fileArgs, fsPath, (err, fileStoreUrl, fileRef)-> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" - return callback(err) - callback(null, fileStoreUrl, fileRef) - - _addFileAndSendToTpds: (project_id, folder_id, fileRef, callback = (error) ->)-> - ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> - if err? - logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileRef.name, fileRef:fileRef, "error adding file with project" - return callback(err) - TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> - return callback(err) if err? - callback(null, result, project) - - addFile: wrapWithLock - beforeLock: (next) -> - (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) -> - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") - ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, (error, fileStoreUrl, fileRef) -> - return callback(error) if error? - next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) - withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)-> - ProjectEntityUpdateHandler._addFileAndSendToTpds project_id, folder_id, fileRef, (err, result, project) -> - return callback(err) if err? - projectHistoryId = project.overleaf?.history?.id - newFiles = [ - file: fileRef - path: result?.path?.fileSystem - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject: project}, (error) -> - return callback(error) if error? - callback(null, fileRef, folder_id) - - replaceFile: wrapWithLock - beforeLock: (next) -> - (project_id, file_id, fsPath, linkedFileData, userId, callback)-> - # create a new file - fileArgs = - name: "dummy-upload-filename" - linkedFileData: linkedFileData - FileStoreHandler.uploadFileFromDisk project_id, fileArgs, fsPath, (err, fileStoreUrl, fileRef)-> - return callback(err) if err? - next project_id, file_id, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback - withLock: (project_id, file_id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback)-> - ProjectEntityMongoUpdateHandler.replaceFileWithNew project_id, file_id, newFileRef, (err, oldFileRef, project, path, newProject) -> - return callback(err) if err? - oldFiles = [ - file: oldFileRef - path: path.fileSystem - ] - newFiles = [ - file: newFileRef - path: path.fileSystem - url: fileStoreUrl - ] - projectHistoryId = project.overleaf?.history?.id - # Increment the rev for an in-place update (with the same path) so the third-party-datastore - # knows this is a new file. - # Ideally we would get this from ProjectEntityMongoUpdateHandler.replaceFileWithNew - # but it returns the original oldFileRef (after incrementing the rev value in mongo), - # so we add 1 to the rev from that. This isn't atomic and relies on the lock - # but it is acceptable for now. - TpdsUpdateSender.addFile {project_id:project._id, file_id:newFileRef._id, path:path.fileSystem, rev:oldFileRef.rev + 1, project_name:project.name}, (err) -> - return callback(err) if err? - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {oldFiles, newFiles, newProject}, callback - - upsertDoc: wrapWithLock (project_id, folder_id, docName, docLines, source, userId, callback = (err, doc, folder_id, isNewDoc)->)-> - if not SafePath.isCleanFilename docName - return callback new Errors.InvalidNameError("invalid element name") - ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> - return callback(error) if error? - return callback(new Error("Couldn't find folder")) if !folder? - existingDoc = null - for doc in folder.docs - if doc.name == docName - existingDoc = doc - break - if existingDoc? - DocumentUpdaterHandler.setDocument project_id, existingDoc._id, userId, docLines, source, (err)=> - logger.log project_id:project_id, doc_id:existingDoc._id, "notifying users that the document has been updated" - DocumentUpdaterHandler.flushDocToMongo project_id, existingDoc._id, (err) -> - return callback(err) if err? - callback null, existingDoc, !existingDoc? - else - self.addDocWithRanges.withoutLock project_id, folder_id, docName, docLines, {}, userId, (err, doc) -> - return callback(err) if err? - callback null, doc, !existingDoc? - - upsertFile: wrapWithLock - beforeLock: (next) -> - (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback)-> - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") - # create a new file - fileArgs = - name: fileName - linkedFileData: linkedFileData - FileStoreHandler.uploadFileFromDisk project_id, fileArgs, fsPath, (err, fileStoreUrl, fileRef)-> - return callback(err) if err? - next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) - withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback = (err, file, isNewFile, existingFile)->)-> - ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> - return callback(error) if error? - return callback(new Error("Couldn't find folder")) if !folder? - existingFile = null - for fileRef in folder.fileRefs - if fileRef.name == fileName - existingFile = fileRef - break - if existingFile? - # this calls directly into the replaceFile main task (without the beforeLock part) - self.replaceFile.mainTask project_id, existingFile._id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) -> - return callback(err) if err? - callback null, newFileRef, !existingFile?, existingFile - else - # this calls directly into the addFile main task (without the beforeLock part) - self.addFile.mainTask project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) -> - return callback(err) if err? - callback null, newFileRef, !existingFile?, existingFile - - upsertDocWithPath: wrapWithLock (project_id, elementPath, docLines, source, userId, callback) -> - if not SafePath.isCleanPath elementPath - return callback new Errors.InvalidNameError("invalid element name") - docName = path.basename(elementPath) - folderPath = path.dirname(elementPath) - self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) -> - return callback(err) if err? - self.upsertDoc.withoutLock project_id, folder._id, docName, docLines, source, userId, (err, doc, isNewDoc) -> - return callback(err) if err? - callback null, doc, isNewDoc, newFolders, folder - - upsertFileWithPath: wrapWithLock - beforeLock: (next) -> - (project_id, elementPath, fsPath, linkedFileData, userId, callback)-> - if not SafePath.isCleanPath elementPath - return callback new Errors.InvalidNameError("invalid element name") - fileName = path.basename(elementPath) - folderPath = path.dirname(elementPath) - # create a new file - fileArgs = - name: fileName - linkedFileData: linkedFileData - FileStoreHandler.uploadFileFromDisk project_id, fileArgs, fsPath, (err, fileStoreUrl, fileRef)-> - return callback(err) if err? - next project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback - withLock: (project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) -> - self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) -> - return callback(err) if err? - # this calls directly into the upsertFile main task (without the beforeLock part) - self.upsertFile.mainTask project_id, folder._id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, (err, newFile, isNewFile, existingFile) -> - return callback(err) if err? - callback null, newFile, isNewFile, existingFile, newFolders, folder - - deleteEntity: wrapWithLock (project_id, entity_id, entityType, userId, callback = (error) ->)-> - logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity" - if !entityType? - logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id - return callback("No entityType set") - entityType = entityType.toLowerCase() - ProjectEntityMongoUpdateHandler.deleteEntity project_id, entity_id, entityType, (error, entity, path, projectBeforeDeletion, newProject) -> - return callback(error) if error? - self._cleanUpEntity projectBeforeDeletion, newProject, entity, entityType, path.fileSystem, userId, (error) -> - return callback(error) if error? - TpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:projectBeforeDeletion.name, (error) -> - return callback(error) if error? - callback null, entity_id - - deleteEntityWithPath: wrapWithLock (project_id, path, userId, callback) -> - ProjectLocator.findElementByPath project_id: project_id, path: path, (err, element, type)-> - return callback(err) if err? - return callback(new Errors.NotFoundError("project not found")) if !element? - self.deleteEntity.withoutLock project_id, element._id, type, userId, callback - - mkdirp: wrapWithLock (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)-> - for folder in path.split('/') - if folder.length > 0 and not SafePath.isCleanFilename folder - return callback new Errors.InvalidNameError("invalid element name") - ProjectEntityMongoUpdateHandler.mkdirp project_id, path, {exactCaseMatch: false}, callback - - mkdirpWithExactCase: wrapWithLock (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)-> - for folder in path.split('/') - if folder.length > 0 and not SafePath.isCleanFilename folder - return callback new Errors.InvalidNameError("invalid element name") - ProjectEntityMongoUpdateHandler.mkdirp project_id, path, {exactCaseMatch: true}, callback - - addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) -> - if not SafePath.isCleanFilename folderName - return callback new Errors.InvalidNameError("invalid element name") - ProjectEntityMongoUpdateHandler.addFolder project_id, parentFolder_id, folderName, callback - - moveEntity: wrapWithLock (project_id, entity_id, destFolderId, entityType, userId, callback = (error) ->)-> - logger.log {entityType, entity_id, project_id, destFolderId}, "moving entity" - if !entityType? - logger.err {err: "No entityType set", project_id, entity_id} - return callback("No entityType set") - entityType = entityType.toLowerCase() - ProjectEntityMongoUpdateHandler.moveEntity project_id, entity_id, destFolderId, entityType, (err, project, startPath, endPath, rev, changes) -> - return callback(err) if err? - projectHistoryId = project.overleaf?.history?.id - TpdsUpdateSender.moveEntity { project_id, project_name: project.name, startPath, endPath, rev } - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback - - renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, userId, callback)-> - if not SafePath.isCleanFilename newName - return callback new Errors.InvalidNameError("invalid element name") - logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType)) - if !entityType? - logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id - return callback("No entityType set") - entityType = entityType.toLowerCase() - - ProjectEntityMongoUpdateHandler.renameEntity project_id, entity_id, entityType, newName, (err, project, startPath, endPath, rev, changes) -> - return callback(err) if err? - projectHistoryId = project.overleaf?.history?.id - TpdsUpdateSender.moveEntity { project_id, project_name: project.name, startPath, endPath, rev } - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback - - # This doesn't directly update project structure but we need to take the lock - # to prevent anything else being queued before the resync update - resyncProjectHistory: wrapWithLock (project_id, callback) -> - ProjectGetter.getProject project_id, rootFolder: true, overleaf: true, (error, project) -> - return callback(error) if error? - - projectHistoryId = project?.overleaf?.history?.id - if !projectHistoryId? - error = new Errors.ProjectHistoryDisabledError("project history not enabled for #{project_id}") - return callback(error) - - ProjectEntityHandler.getAllEntitiesFromProject project, (error, docs, files) -> - return callback(error) if error? - - docs = _.map docs, (doc) -> - doc: doc.doc._id - path: doc.path - - files = _.map files, (file) -> - file: file.file._id - path: file.path - url: FileStoreHandler._buildUrl(project_id, file.file._id) - - DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback - - _cleanUpEntity: (project, newProject, entity, entityType, path, userId, callback = (error) ->) -> - self._updateProjectStructureWithDeletedEntity project, newProject, entity, entityType, path, userId, (error) -> - return callback(error) if error? - if(entityType.indexOf("file") != -1) - self._cleanUpFile project, entity, path, userId, callback - else if (entityType.indexOf("doc") != -1) - self._cleanUpDoc project, entity, path, userId, callback - else if (entityType.indexOf("folder") != -1) - self._cleanUpFolder project, entity, path, userId, callback - else - callback() - - # Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity - # methods both need to recursively iterate over the entities in folder. - # These are currently using separate implementations of the recursion. In - # future, these could be simplified using a common project entity iterator. - _updateProjectStructureWithDeletedEntity: (project, newProject, entity, entityType, entityPath, userId, callback = (error) ->) -> - # compute the changes to the project structure - if(entityType.indexOf("file") != -1) - changes = oldFiles: [ {file: entity, path: entityPath} ] - else if (entityType.indexOf("doc") != -1) - changes = oldDocs: [ {doc: entity, path: entityPath} ] - else if (entityType.indexOf("folder") != -1) - changes = {oldDocs: [], oldFiles: []} - _recurseFolder = (folder, folderPath) -> - for doc in folder.docs - changes.oldDocs.push {doc, path: path.join(folderPath, doc.name)} - for file in folder.fileRefs - changes.oldFiles.push {file, path: path.join(folderPath, file.name)} - for childFolder in folder.folders - _recurseFolder(childFolder, path.join(folderPath, childFolder.name)) - _recurseFolder entity, entityPath - # now send the project structure changes to the docupdater - changes.newProject = newProject - project_id = project._id.toString() - projectHistoryId = project.overleaf?.history?.id - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback - - _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) -> - project_id = project._id.toString() - doc_id = doc._id.toString() - unsetRootDocIfRequired = (callback) => - if project.rootDoc_id? and project.rootDoc_id.toString() == doc_id - @unsetRootDoc project_id, callback - else - callback() - - unsetRootDocIfRequired (error) -> - return callback(error) if error? - ProjectEntityMongoUpdateHandler._insertDeletedDocReference project._id, doc, (error) -> - return callback(error) if error? - DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) -> - return callback(error) if error? - DocstoreManager.deleteDoc project_id, doc_id, callback - - _cleanUpFile: (project, file, path, userId, callback = (error) ->) -> - ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, callback - - _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) -> - jobs = [] - for doc in folder.docs - do (doc) -> - docPath = path.join(folderPath, doc.name) - jobs.push (callback) -> self._cleanUpDoc project, doc, docPath, userId, callback - - for file in folder.fileRefs - do (file) -> - filePath = path.join(folderPath, file.name) - jobs.push (callback) -> self._cleanUpFile project, file, filePath, userId, callback - - for childFolder in folder.folders - do (childFolder) -> - folderPath = path.join(folderPath, childFolder.name) - jobs.push (callback) -> self._cleanUpFolder project, childFolder, folderPath, userId, callback - - async.series jobs, callback diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee deleted file mode 100644 index 81d2090bc0..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ /dev/null @@ -1,98 +0,0 @@ -mongojs = require("../../infrastructure/mongojs") -metrics = require("metrics-sharelatex") -db = mongojs.db -ObjectId = mongojs.ObjectId -async = require "async" -Project = require("../../models/Project").Project -logger = require("logger-sharelatex") -LockManager = require("../../infrastructure/LockManager") - -module.exports = ProjectGetter = - EXCLUDE_DEPTH: 8 - - getProjectWithoutDocLines: (project_id, callback=(error, project) ->) -> - excludes = {} - for i in [1..ProjectGetter.EXCLUDE_DEPTH] - excludes["rootFolder#{Array(i).join(".folders")}.docs.lines"] = 0 - ProjectGetter.getProject project_id, excludes, callback - - getProjectWithOnlyFolders: (project_id, callback=(error, project) ->) -> - excludes = {} - for i in [1..ProjectGetter.EXCLUDE_DEPTH] - excludes["rootFolder#{Array(i).join(".folders")}.docs"] = 0 - excludes["rootFolder#{Array(i).join(".folders")}.fileRefs"] = 0 - ProjectGetter.getProject project_id, excludes, callback - - getProject: (project_id, projection, callback) -> - if typeof(projection) == "function" && !callback? - callback = projection - projection = {} - if !project_id? - return callback(new Error("no project_id provided")) - if typeof(projection) != "object" - return callback(new Error("projection is not an object")) - - if projection?.rootFolder || Object.keys(projection).length == 0 - ProjectEntityMongoUpdateHandler = require './ProjectEntityMongoUpdateHandler' - LockManager.runWithLock ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE, project_id, - (cb) -> ProjectGetter.getProjectWithoutLock project_id, projection, cb - callback - else - ProjectGetter.getProjectWithoutLock project_id, projection, callback - - getProjectWithoutLock: (project_id, projection, callback) -> - if typeof(projection) == "function" && !callback? - callback = projection - projection = {} - if !project_id? - return callback(new Error("no project_id provided")) - if typeof(projection) != "object" - return callback(new Error("projection is not an object")) - - if typeof project_id == "string" - query = _id: ObjectId(project_id) - else if project_id instanceof ObjectId - query = _id: project_id - else if project_id?.toString().length == 24 # sometimes mongoose ids are hard to identify, this will catch them - query = _id: ObjectId(project_id.toString()) - else - err = new Error("malformed get request") - logger.log project_id:project_id, err:err, type:typeof(project_id), "malformed get request" - return callback(err) - - db.projects.find query, projection, (err, project) -> - if err? - logger.err err:err, query:query, projection:projection, "error getting project" - return callback(err) - callback(null, project?[0]) - - getProjectIdByReadAndWriteToken: (token, callback=(err, project_id)->) -> - Project.findOne {'tokens.readAndWrite': token}, {_id: 1}, (err, project) -> - return callback err if err? - return callback() unless project? - callback null, project._id - - findAllUsersProjects: ( - user_id, - fields, - callback = (error, projects={owned: [], readAndWrite: [], readOnly: [], tokenReadAndWrite: [], tokenReadOnly: []}) -> - ) -> - CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" - Project.find {owner_ref: user_id}, fields, (error, ownedProjects) -> - return callback(error) if error? - CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, projects) -> - return callback(error) if error? - result = { - owned: ownedProjects || [], - readAndWrite: projects.readAndWrite || [], - readOnly: projects.readOnly || [], - tokenReadAndWrite: projects.tokenReadAndWrite || [], - tokenReadOnly: projects.tokenReadOnly || [] - } - callback null, result - -[ - 'getProject', - 'getProjectWithoutDocLines' -].map (method) -> - metrics.timeAsyncMethod(ProjectGetter, method, 'mongo.ProjectGetter', logger) diff --git a/services/web/app/coffee/Features/Project/ProjectHelper.coffee b/services/web/app/coffee/Features/Project/ProjectHelper.coffee deleted file mode 100644 index 69b4daec5d..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectHelper.coffee +++ /dev/null @@ -1,56 +0,0 @@ -ENGINE_TO_COMPILER_MAP = { - latex_dvipdf: "latex" - pdflatex: "pdflatex" - xelatex: "xelatex" - lualatex: "lualatex" -} - -module.exports = ProjectHelper = - compilerFromV1Engine: (engine) -> - return ENGINE_TO_COMPILER_MAP[engine] - - ensureNameIsUnique: (nameList, name, suffixes = [], maxLength, callback = (error, name, changed)->) -> - # create a set of all project names - allNames = new Set(nameList) - isUnique = (x) -> !allNames.has(x) - # check if the supplied name is already unique - if isUnique(name) - return callback(null, name, false) - # the name already exists, try adding the user-supplied suffixes to generate a unique name - for suffix in suffixes - candidateName = ProjectHelper._addSuffixToProjectName(name, suffix, maxLength) - if isUnique(candidateName) - return callback(null, candidateName, true) - # if there are no (more) suffixes, use a numeric one - uniqueName = ProjectHelper._addNumericSuffixToProjectName(name, allNames, maxLength) - if uniqueName? - callback(null, uniqueName, true) - else - callback(new Error("Failed to generate a unique name for: #{name}")) - - _addSuffixToProjectName: (name, suffix = '', maxLength) -> - # append the suffix and truncate the project title if needed - truncatedLength = maxLength - suffix.length - return name.substr(0, truncatedLength) + suffix - - _addNumericSuffixToProjectName: (name, allProjectNames, maxLength) -> - NUMERIC_SUFFIX_MATCH = / \((\d+)\)$/ - suffixedName = (basename, number) -> - suffix = " (#{number})" - return basename.substr(0, maxLength - suffix.length) + suffix - - match = name.match(NUMERIC_SUFFIX_MATCH) - basename = name - n = 1 - last = allProjectNames.size + n - - if match? - basename = name.replace(NUMERIC_SUFFIX_MATCH, '') - n = parseInt(match[1]) - - while n <= last - candidate = suffixedName(basename, n) - return candidate unless allProjectNames.has(candidate) - n += 1 - - return null \ No newline at end of file diff --git a/services/web/app/coffee/Features/Project/ProjectHistoryHandler.coffee b/services/web/app/coffee/Features/Project/ProjectHistoryHandler.coffee deleted file mode 100644 index ab9ec77eb5..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectHistoryHandler.coffee +++ /dev/null @@ -1,55 +0,0 @@ -Project = require('../../models/Project').Project -ProjectDetailsHandler = require "./ProjectDetailsHandler" -logger = require('logger-sharelatex') -settings = require("settings-sharelatex") -HistoryManager = require "../History/HistoryManager" -ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler" - -module.exports = ProjectHistoryHandler = - - setHistoryId: (project_id, history_id, callback = (err) ->) -> - # reject invalid history ids - return callback(new Error("invalid history id")) if !history_id or typeof(history_id) isnt 'number' - # use $exists:false to prevent overwriting any existing history id, atomically - Project.update {_id: project_id, "overleaf.history.id": {$exists:false}}, {"overleaf.history.id":history_id}, (err, result)-> - return callback(err) if err? - return callback(new Error("history exists")) if result?.n == 0 - callback() - - getHistoryId: (project_id, callback = (err, result) ->) -> - ProjectDetailsHandler.getDetails project_id, (err, project) -> - return callback(err) if err? # n.b. getDetails returns an error if the project doesn't exist - callback(null, project?.overleaf?.history?.id) - - upgradeHistory: (project_id, callback = (err, result) ->) -> - # project must have an overleaf.history.id before allowing display of new history - Project.update {_id: project_id, "overleaf.history.id": {$exists:true}}, {"overleaf.history.display":true, "overleaf.history.upgradedAt": new Date()}, (err, result)-> - return callback(err) if err? - # return an error if overleaf.history.id wasn't present - return callback(new Error("history not upgraded")) if result?.n == 0 - callback() - - downgradeHistory: (project_id, callback = (err, result) ->) -> - Project.update {_id: project_id, "overleaf.history.upgradedAt": {$exists:true}}, {"overleaf.history.display":false, $unset:{"overleaf.history.upgradedAt":1}}, (err, result)-> - return callback(err) if err? - return callback(new Error("history not downgraded")) if result?.n == 0 - callback() - - ensureHistoryExistsForProject: (project_id, callback = (err) ->) -> - # We can only set a history id for a project that doesn't have one. The - # history id is cached in the project history service, and changing an - # existing value corrupts the history, leaving it in an irrecoverable - # state. Setting a history id when one wasn't present before is ok, - # because undefined history ids aren't cached. - ProjectHistoryHandler.getHistoryId project_id, (err, history_id) -> - return callback(err) if err? - return callback() if history_id? # history already exists, success - HistoryManager.initializeProject (err, history) -> - return callback(err) if err? - return callback(new Error("failed to initialize history id")) if !history?.overleaf_id - ProjectHistoryHandler.setHistoryId project_id, history.overleaf_id, (err) -> - return callback(err) if err? - ProjectEntityUpdateHandler.resyncProjectHistory project_id, (err) -> - return callback(err) if err? - logger.log {project_id: project_id, history_id: history.overleaf_id}, "started syncing project with new history id" - HistoryManager.flushProject project_id, callback diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee deleted file mode 100644 index c23faeff90..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ /dev/null @@ -1,193 +0,0 @@ -Project = require('../../models/Project').Project -ProjectGetter = require("./ProjectGetter") -Errors = require "../Errors/Errors" -_ = require('underscore') -logger = require('logger-sharelatex') -async = require('async') - -module.exports = ProjectLocator = - findElement: (options, _callback = (err, element, path, parentFolder)->)-> - callback = (args...) -> - _callback(args...) - _callback = () -> - - {project, project_id, element_id, type} = options - elementType = sanitizeTypeOfElement type - - count = 0 - endOfBranch = -> - if --count == 0 - logger.warn "element #{element_id} could not be found for project #{project_id || project._id}" - return callback(new Errors.NotFoundError("entity not found")) - - search = (searchFolder, path)-> - count++ - element = _.find searchFolder[elementType], (el)-> el?._id+'' == element_id+'' #need to ToString both id's for robustness - if !element? && searchFolder.folders? && searchFolder.folders.length != 0 - _.each searchFolder.folders, (folder, index)-> - if !folder? - return - newPath = {} - newPath[key] = value for own key,value of path #make a value copy of the string - newPath.fileSystem += "/#{folder.name}" - newPath.mongo += ".folders.#{index}" - search folder, newPath - endOfBranch() - return - else if element? - elementPlaceInArray = getIndexOf(searchFolder[elementType], element_id) - path.fileSystem += "/#{element.name}" - path.mongo +=".#{elementType}.#{elementPlaceInArray}" - callback(null, element, path, searchFolder) - else if !element? - return endOfBranch() - - path = {fileSystem:'',mongo:'rootFolder.0'} - - startSearch = (project)-> - if element_id+'' == project.rootFolder[0]._id+'' - callback(null, project.rootFolder[0], path, null) - else - search project.rootFolder[0], path - - if project? - startSearch(project) - else - ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)-> - return callback(err) if err? - if !project? - return callback(new Errors.NotFoundError("project not found")) - startSearch project - - findRootDoc : (opts, callback)-> - getRootDoc = (project)=> - if project.rootDoc_id? - @findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, (error, args...) -> - if error? - if error instanceof Errors.NotFoundError - return callback null, null - else - return callback error - return callback null, args... - else - callback null, null - {project, project_id} = opts - if project? - getRootDoc project - else - ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)-> - if err? - logger.err err:err, "error getting project" - return callback(err) - else - getRootDoc project - - findElementByPath: (options, callback = (err, foundEntity, type)->)-> - {project, project_id, path, exactCaseMatch} = options - if !path? - return new Error('no path provided for findElementByPath') - - if project? - ProjectLocator._findElementByPathWithProject project, path, exactCaseMatch, callback - else - ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)-> - return callback(err) if err? - ProjectLocator._findElementByPathWithProject project, path, exactCaseMatch, callback - - _findElementByPathWithProject: (project, needlePath, exactCaseMatch, callback = (err, foundEntity, type)->)-> - if exactCaseMatch - matchFn = (a, b) -> (a == b) - else - matchFn = (a, b) -> (a?.toLowerCase() == b?.toLowerCase()) - - getParentFolder = (haystackFolder, foldersList, level, cb)-> - if foldersList.length == 0 - return cb null, haystackFolder - needleFolderName = foldersList[level] - found = false - for folder in haystackFolder.folders - if matchFn(folder.name, needleFolderName) - found = true - if level == foldersList.length-1 - return cb null, folder - else - return getParentFolder(folder, foldersList, level+1, cb) - if !found - cb("not found project: #{project._id} search path: #{needlePath}, folder #{foldersList[level]} could not be found") - - getEntity = (folder, entityName, cb)-> - if !entityName? - return cb null, folder, "folder" - for file in folder.fileRefs or [] - if matchFn(file?.name, entityName) - result = file - type = "file" - for doc in folder.docs or [] - if matchFn(doc?.name, entityName) - result = doc - type = "doc" - for childFolder in folder.folders or [] - if matchFn(childFolder?.name, entityName) - result = childFolder - type = "folder" - - if result? - cb null, result, type - else - cb("not found project: #{project._id} search path: #{needlePath}, entity #{entityName} could not be found") - - - if err? - logger.err err:err, project_id:project._id, "error getting project for finding element" - return callback(err) - if !project? - return callback("project could not be found for finding a element #{project._id}") - if needlePath == '' || needlePath == '/' - return callback(null, project.rootFolder[0], "folder") - - if needlePath.indexOf('/') == 0 - needlePath = needlePath.substring(1) - foldersList = needlePath.split('/') - needleName = foldersList.pop() - rootFolder = project.rootFolder[0] - - logger.log project_id:project._id, path:needlePath, foldersList:foldersList, "looking for element by path" - jobs = new Array() - jobs.push( - (cb)-> - getParentFolder rootFolder, foldersList, 0, cb - ) - jobs.push( - (folder, cb)-> - getEntity folder, needleName, cb - ) - async.waterfall jobs, callback - - findUsersProjectByName: (user_id, projectName, callback)-> - ProjectGetter.findAllUsersProjects user_id, 'name archived', (err, allProjects)-> - return callback(error) if error? - {owned, readAndWrite} = allProjects - projects = owned.concat(readAndWrite) - projectName = projectName.toLowerCase() - project = _.find projects, (project)-> - project.name.toLowerCase() == projectName and project.archived != true - logger.log user_id:user_id, projectName:projectName, totalProjects:projects.length, project:project, "looking for project by name" - callback(null, project) - - -sanitizeTypeOfElement = (elementType)-> - lastChar = elementType.slice -1 - if lastChar != "s" - elementType +="s" - if elementType == "files" - elementType = "fileRefs" - return elementType - - -getIndexOf = (searchEntity, id)-> - length = searchEntity.length - count = 0 - while(count < length) - if searchEntity[count]?._id+"" == id+"" - return count - count++ diff --git a/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee deleted file mode 100644 index 7bdad93b03..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectOptionsHandler.coffee +++ /dev/null @@ -1,69 +0,0 @@ -Project = require('../../models/Project').Project -logger = require('logger-sharelatex') -_ = require('underscore') -settings = require("settings-sharelatex") - -safeCompilers = ["xelatex", "pdflatex", "latex", "lualatex"] - -module.exports = - setCompiler : (project_id, compiler, callback = ()->)-> - logger.log project_id:project_id, compiler:compiler, "setting the compiler" - compiler = compiler.toLowerCase() - if !_.contains safeCompilers, compiler - return callback() - conditions = {_id:project_id} - update = {compiler:compiler} - Project.update conditions, update, {}, (err)-> - if callback? - callback() - - setImageName : (project_id, imageName, callback = ()->)-> - logger.log project_id:project_id, imageName:imageName, "setting the imageName" - imageName = imageName.toLowerCase() - if ! _.some(settings.allowedImageNames, (allowed) -> imageName is allowed.imageName) - return callback() - conditions = {_id:project_id} - update = {imageName: settings.imageRoot + '/' + imageName} - Project.update conditions, update, {}, (err)-> - if callback? - callback() - - setSpellCheckLanguage: (project_id, languageCode, callback = ()->)-> - logger.log project_id:project_id, languageCode:languageCode, "setting the spell check language" - languageIsSafe = false - settings.languages.forEach (safeLang)-> - if safeLang.code == languageCode - languageIsSafe = true - - if languageCode == "" - languageIsSafe = true - - if languageIsSafe - conditions = {_id:project_id} - update = {spellCheckLanguage:languageCode} - Project.update conditions, update, {}, (err)-> - callback() - else - logger.err project_id:project_id, languageCode:languageCode, "tryed to set unsafe language" - callback() - - setBrandVariationId: (project_id, brandVariationId, callback = ()->)-> - logger.log project_id:project_id, brandVariationId:brandVariationId, "setting the brand variation id" - if !brandVariationId? or brandVariationId == "" - return callback() - conditions = {_id:project_id} - update = {brandVariationId} - Project.update conditions, update, {}, (err)-> - if err? - logger.err err:err, "error setting brandVariationId" - callback() - - unsetBrandVariationId: (project_id, callback = ()->)-> - logger.log project_id:project_id, "unsetting the brand variation id" - conditions = {_id:project_id} - update = {$unset: {brandVariationId: 1}} - Project.update conditions, update, {}, (err)-> - if err? - logger.err err:err, "error unsetting brandVariationId" - return callback(err) - callback() diff --git a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee deleted file mode 100644 index edc39f7436..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee +++ /dev/null @@ -1,155 +0,0 @@ -ProjectEntityHandler = require "./ProjectEntityHandler" -ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler" -ProjectGetter = require "./ProjectGetter" -DocumentHelper = require "../Documents/DocumentHelper" -Path = require "path" -fs = require("fs") -async = require("async") -globby = require("globby") -_ = require("underscore") - -module.exports = ProjectRootDocManager = - setRootDocAutomatically: (project_id, callback = (error) ->) -> - ProjectEntityHandler.getAllDocs project_id, (error, docs) -> - return callback(error) if error? - - jobs = _.map docs, (doc, path)-> - return (cb)-> - if /\.R?tex$/.test(Path.extname(path)) && DocumentHelper.contentHasDocumentclass(doc.lines) - cb(doc._id) - else - cb(null) - - async.series jobs, (root_doc_id)-> - if root_doc_id? - ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback - else - callback() - - findRootDocFileFromDirectory: (directoryPath, callback = (error, path, content) ->) -> - filePathsPromise = globby([ - '**/*.{tex,Rtex}' - ], { - cwd: directoryPath, - followSymlinkedDirectories: false, - onlyFiles: true, - case: false - } - ) - - # the search order is such that we prefer files closer to the project root, then - # we go by file size in ascending order, because people often have a main - # file that just includes a bunch of other files; then we go by name, in - # order to be deterministic - filePathsPromise.then( - (unsortedFiles) -> - ProjectRootDocManager._sortFileList unsortedFiles, directoryPath, (err, files) -> - return callback(err) if err? - doc = null - - async.until( - -> - return doc? || files.length == 0 - (cb) -> - file = files.shift() - fs.readFile Path.join(directoryPath, file), 'utf8', (error, content) -> - return cb(error) if error? - content = (content || '').replace(/\r/g, '') - if DocumentHelper.contentHasDocumentclass(content) - doc = {path: file, content: content} - cb(null) - (err) -> - callback(err, doc?.path, doc?.content) - ) - (err) -> - callback(err) - ) - - # coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha - return null - - setRootDocFromName: (project_id, rootDocName, callback = (error) ->) -> - ProjectEntityHandler.getAllDocPathsFromProjectById project_id, (error, docPaths) -> - return callback(error) if error? - # strip off leading and trailing quotes from rootDocName - rootDocName = rootDocName.replace(/^\'|\'$/g,"") - # prepend a slash for the root folder if not present - rootDocName = "/#{rootDocName}" if rootDocName[0] isnt '/' - # find the root doc from the filename - root_doc_id = null - for doc_id, path of docPaths - # docpaths have a leading / so allow matching "folder/filename" and "/folder/filename" - if path == rootDocName - root_doc_id = doc_id - # try a basename match if there was no match - if !root_doc_id - for doc_id, path of docPaths - if Path.basename(path) == Path.basename(rootDocName) - root_doc_id = doc_id - # set the root doc id if we found a match - if root_doc_id? - ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback - else - callback() - - ensureRootDocumentIsSet: (project_id, callback = (error) ->) -> - ProjectGetter.getProject project_id, rootDoc_id: 1, (error, project) -> - return callback(error) if error? - if !project? - return callback new Error("project not found") - - if project.rootDoc_id? - callback() - else - ProjectRootDocManager.setRootDocAutomatically project_id, callback - - ensureRootDocumentIsValid: (project_id, callback = (error) ->) -> - ProjectGetter.getProject project_id, rootDoc_id: 1, (error, project) -> - return callback(error) if error? - if !project? - return callback new Error("project not found") - - if project.rootDoc_id? - ProjectEntityHandler.getAllDocPathsFromProjectById project_id, (error, docPaths) -> - return callback(error) if error? - rootDocValid = false - for doc_id, _path of docPaths - if doc_id == project.rootDoc_id - rootDocValid = true - if rootDocValid - callback() - else - ProjectEntityUpdateHandler.setRootDoc project_id, null, -> - ProjectRootDocManager.setRootDocAutomatically project_id, callback - else - ProjectRootDocManager.setRootDocAutomatically project_id, callback - - _sortFileList: (listToSort, rootDirectory, callback = (error, result)->) -> - async.mapLimit( - listToSort - 5 - (filePath, cb) -> - fs.stat Path.join(rootDirectory, filePath), (err, stat) -> - return cb(err) if err? - cb(null, - size: stat.size - path: filePath - elements: filePath.split(Path.sep).length - name: Path.basename(filePath) - ) - (err, files) -> - return callback(err) if err? - - callback(null, _.map files.sort(ProjectRootDocManager._rootDocSort), (file)-> return file.path) - ) - - _rootDocSort: (a, b) -> - # sort first by folder depth - return a.elements - b.elements if a.elements != b.elements - # ensure main.tex is at the start of each folder - return -1 if (a.name == 'main.tex' && b.name != 'main.tex') - return 1 if (a.name != 'main.tex' && b.name == 'main.tex') - # prefer smaller files - return a.size - b.size if a.size != b.size - # otherwise, use the full path name - return a.path.localeCompare(b.path) diff --git a/services/web/app/coffee/Features/Project/ProjectTokenGenerator.coffee b/services/web/app/coffee/Features/Project/ProjectTokenGenerator.coffee deleted file mode 100644 index 43c6a2b3fd..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectTokenGenerator.coffee +++ /dev/null @@ -1,66 +0,0 @@ -crypto = require 'crypto' -V1Api = require('../V1/V1Api') -Async = require('async') -logger = require('logger-sharelatex') - - -# This module mirrors the token generation in Overleaf (`random_token.rb`), -# for the purposes of implementing token-based project access, like the -# 'unlisted-projects' feature in Overleaf - -module.exports = ProjectTokenGenerator = - - # (From Overleaf `random_token.rb`) - # Letters (not numbers! see generate_token) used in tokens. They're all - # consonants, to avoid embarassing words (I can't think of any that use only - # a y), and lower case "l" is omitted, because in many fonts it is - # indistinguishable from an upper case "I" (and sometimes even the number 1). - TOKEN_ALPHA: 'bcdfghjkmnpqrstvwxyz' - TOKEN_NUMERICS: '123456789' - - _randomString: (length, alphabet) -> - result = crypto.randomBytes(length).toJSON().data.map( - (b) -> alphabet[b % alphabet.length] - ).join('') - return result - - # Generate a 12-char token with only characters from TOKEN_ALPHA, - # suitable for use as a read-only token for a project - readOnlyToken: () -> - return ProjectTokenGenerator._randomString( - 12, - ProjectTokenGenerator.TOKEN_ALPHA - ) - - # Generate a longer token, with a numeric prefix, - # suitable for use as a read-and-write token for a project - readAndWriteToken: () -> - numerics = ProjectTokenGenerator._randomString( - 10, - ProjectTokenGenerator.TOKEN_NUMERICS - ) - token = ProjectTokenGenerator._randomString( - 12, - ProjectTokenGenerator.TOKEN_ALPHA - ) - fullToken = "#{numerics}#{token}" - return { token: fullToken, numericPrefix: numerics } - - generateUniqueReadOnlyToken: (callback=(err, token)->) -> - Async.retry 10 - , (cb) -> - token = ProjectTokenGenerator.readOnlyToken() - logger.log {token}, "Generated read-only token" - V1Api.request { - url: "/api/v1/sharelatex/docs/read_token/#{token}/exists", - json: true - }, (err, response, body) -> - return cb(err) if err? - if response.statusCode != 200 - return cb(new Error("non-200 response from v1 read-token-exists api: #{response.statusCode}")) - if body.exists == true - cb(new Error("token already exists in v1: #{token}")) - else - logger.log {token}, "Read-only token does not exist in v1, good to use" - cb(null, token) - , callback diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee deleted file mode 100644 index e0f2c5f7af..0000000000 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ /dev/null @@ -1,37 +0,0 @@ -Project = require('../../models/Project').Project -logger = require('logger-sharelatex') - -module.exports = - markAsUpdated : (projectId, lastUpdatedAt, lastUpdatedBy, callback = () ->)-> - lastUpdatedAt ?= new Date() - - conditions = - _id: projectId - lastUpdated: { $lt: lastUpdatedAt } - - update = { - lastUpdated: lastUpdatedAt or (new Date()).getTime() - lastUpdatedBy: lastUpdatedBy - } - Project.update conditions, update, {}, callback - - markAsOpened : (project_id, callback)-> - conditions = {_id:project_id} - update = {lastOpened:Date.now()} - Project.update conditions, update, {}, (err)-> - if callback? - callback() - - markAsInactive: (project_id, callback)-> - conditions = {_id:project_id} - update = {active:false} - Project.update conditions, update, {}, (err)-> - if callback? - callback() - - markAsActive: (project_id, callback)-> - conditions = {_id:project_id} - update = {active:true} - Project.update conditions, update, {}, (err)-> - if callback? - callback() diff --git a/services/web/app/coffee/Features/Project/SafePath.coffee b/services/web/app/coffee/Features/Project/SafePath.coffee deleted file mode 100644 index 82aef24bb1..0000000000 --- a/services/web/app/coffee/Features/Project/SafePath.coffee +++ /dev/null @@ -1,97 +0,0 @@ -# This file is shared between the frontend and server code of web, so that -# filename validation is the same in both implementations. -# Both copies must be kept in sync: -# app/coffee/Features/Project/SafePath.coffee -# public/coffee/ide/directives/SafePath.coffee - -load = () -> - BADCHAR_RX = /// - [ - \/ # no forward slashes - \\ # no back slashes - \* # no asterisk - \u0000-\u001F # no control characters (0-31) - \u007F # no delete - \u0080-\u009F # no unicode control characters (C1) - \uD800-\uDFFF # no unicode surrogate characters - ] - ///g - - BADFILE_RX = /// - (^\.$) # reject . as a filename - | (^\.\.$) # reject .. as a filename - | (^\s+) # reject leading space - | (\s+$) # reject trailing space - ///g - - # Put a block on filenames which match javascript property names, as they - # can cause exceptions where the code puts filenames into a hash. This is a - # temporary workaround until the code in other places is made safe against - # property names. - # - # The list of property names is taken from - # ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype)) - BLOCKEDFILE_RX = /// - ^( - prototype - |constructor - |toString - |toLocaleString - |valueOf - |hasOwnProperty - |isPrototypeOf - |propertyIsEnumerable - |__defineGetter__ - |__lookupGetter__ - |__defineSetter__ - |__lookupSetter__ - |__proto__ - )$ - /// - - MAX_PATH = 1024 # Maximum path length, in characters. This is fairly arbitrary. - - SafePath = - # convert any invalid characters to underscores in the given filename - clean: (filename) -> - filename = filename.replace BADCHAR_RX, '_' - # for BADFILE_RX replace any matches with an equal number of underscores - filename = filename.replace BADFILE_RX, (match) -> - return new Array(match.length + 1).join("_") - # replace blocked filenames 'prototype' with '@prototype' - filename = filename.replace BLOCKEDFILE_RX, "@$1" - return filename - - # returns whether the filename is 'clean' (does not contain any invalid - # characters or reserved words) - isCleanFilename: (filename) -> - return SafePath.isAllowedLength(filename) && - !BADCHAR_RX.test(filename) && - !BADFILE_RX.test(filename) - - isBlockedFilename: (filename) -> - return BLOCKEDFILE_RX.test(filename) - - # returns whether a full path is 'clean' - e.g. is a full or relative path - # that points to a file, and each element passes the rules in 'isCleanFilename' - isCleanPath: (path) -> - elements = path.split('/') - - lastElementIsEmpty = elements[elements.length - 1].length == 0 - return false if lastElementIsEmpty - - for element in elements - return false if element.length > 0 && !SafePath.isCleanFilename element - - # check for a top-level reserved name - return false if BLOCKEDFILE_RX.test(path.replace(/^\/?/,'')) # remove leading slash if present - - return true - - isAllowedLength: (pathname) -> - return pathname.length > 0 && pathname.length <= MAX_PATH - -if define? - define [], load -else - module.exports = load() diff --git a/services/web/app/coffee/Features/Publishers/PublishersGetter.coffee b/services/web/app/coffee/Features/Publishers/PublishersGetter.coffee deleted file mode 100644 index 701da6cd3a..0000000000 --- a/services/web/app/coffee/Features/Publishers/PublishersGetter.coffee +++ /dev/null @@ -1,9 +0,0 @@ -UserMembershipsHandler = require "../UserMembership/UserMembershipsHandler" -UserMembershipEntityConfigs = require "../UserMembership/UserMembershipEntityConfigs" -logger = require 'logger-sharelatex' -_ = require 'underscore' - -module.exports = PublishersGetter = - getManagedPublishers: (user_id, callback = (error, managedPublishers) ->) -> - UserMembershipsHandler.getEntitiesByUser UserMembershipEntityConfigs.publisher, user_id, (error, managedPublishers) -> - callback(error, managedPublishers) diff --git a/services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee b/services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee deleted file mode 100644 index 0672520f31..0000000000 --- a/services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee +++ /dev/null @@ -1,21 +0,0 @@ -settings = require "settings-sharelatex" - -httpProxy = require('http-proxy'); -proxy = httpProxy.createProxyServer({ - target: settings.apis.realTime.url -}) -wsProxy = httpProxy.createProxyServer({ - target: settings.apis.realTime.url.replace("http://", "ws://") - ws: true -}) - -module.exports = - apply: (webRouter, apiRouter) -> - webRouter.all /\/socket\.io\/.*/, (req, res, next) -> - proxy.web req, res, next - - setTimeout () -> - Server = require "../../infrastructure/Server" - Server.server.on "upgrade", (req, socket, head) -> - wsProxy.ws req, socket, head - , 0 \ No newline at end of file diff --git a/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee b/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee deleted file mode 100644 index cfee62fdfa..0000000000 --- a/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee +++ /dev/null @@ -1,35 +0,0 @@ -_ = require("underscore") -logger = require('logger-sharelatex') -User = require('../../models/User').User -Settings = require "settings-sharelatex" -FeaturesUpdater = require "../Subscription/FeaturesUpdater" - -module.exports = ReferalAllocator = - allocate: (referal_id, new_user_id, referal_source, referal_medium, callback = ->)-> - if !referal_id? - logger.log new_user_id:new_user_id, "no referal for user" - return callback(null) - - logger.log referal_id:referal_id, new_user_id:new_user_id, referal_source:referal_source, referal_medium:referal_medium, "allocating users referal" - - query = {"referal_id":referal_id} - User.findOne query, (error, user) -> - return callback(error) if error? - if !user? or !user._id? - logger.log new_user_id:new_user_id, referal_id:referal_id, "no user found for referal id" - return callback(null) - - if referal_source == "bonus" - User.update query, { - $push: - refered_users: new_user_id - $inc: - refered_user_count: 1 - }, {}, (err)-> - if err? - logger.err err:err, referal_id:referal_id, new_user_id:new_user_id, "something went wrong allocating referal" - return callback(err) - - FeaturesUpdater.refreshFeatures user._id, callback - else - callback() diff --git a/services/web/app/coffee/Features/Referal/ReferalConnect.coffee b/services/web/app/coffee/Features/Referal/ReferalConnect.coffee deleted file mode 100644 index e84474a47e..0000000000 --- a/services/web/app/coffee/Features/Referal/ReferalConnect.coffee +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = - - use: (req, res, next)-> - if req.query? - if req.query.referal? - req.session.referal_id = req.query.referal - else if req.query.r? # Short hand for referal - req.session.referal_id = req.query.r - else if req.query.fb_ref? - req.session.referal_id = req.query.fb_ref - - if req.query.rm? # referal medium e.g. twitter, facebook, email - switch req.query.rm - when "fb" - req.session.referal_medium = "facebook" - when "t" - req.session.referal_medium = "twitter" - when "gp" - req.session.referal_medium = "google_plus" - when "e" - req.session.referal_medium = "email" - when "d" - req.session.referal_medium = "direct" - - if req.query.rs? # referal source e.g. project share, bonus - switch req.query.rs - when "b" - req.session.referal_source = "bonus" - when "ps" - req.session.referal_source = "public_share" - when "ci" - req.session.referal_source = "collaborator_invite" - - next() diff --git a/services/web/app/coffee/Features/Referal/ReferalController.coffee b/services/web/app/coffee/Features/Referal/ReferalController.coffee deleted file mode 100644 index 421a186a9f..0000000000 --- a/services/web/app/coffee/Features/Referal/ReferalController.coffee +++ /dev/null @@ -1,12 +0,0 @@ -logger = require('logger-sharelatex') -ReferalHandler = require('./ReferalHandler') -AuthenticationController = require('../Authentication/AuthenticationController') - -module.exports = - bonus: (req, res)-> - user_id = AuthenticationController.getLoggedInUserId(req) - ReferalHandler.getReferedUsers user_id, (err, refered_users, refered_user_count)-> - res.render "referal/bonus", - title: "bonus_please_recommend_us" - refered_users: refered_users - refered_user_count: refered_user_count diff --git a/services/web/app/coffee/Features/Referal/ReferalFeatures.coffee b/services/web/app/coffee/Features/Referal/ReferalFeatures.coffee deleted file mode 100644 index 0980f06222..0000000000 --- a/services/web/app/coffee/Features/Referal/ReferalFeatures.coffee +++ /dev/null @@ -1,30 +0,0 @@ -_ = require("underscore") -logger = require('logger-sharelatex') -User = require('../../models/User').User -Settings = require "settings-sharelatex" - -module.exports = ReferalFeatures = - getBonusFeatures: (user_id, callback = (error) ->) -> - query = _id: user_id - User.findOne query, (error, user) -> - return callback(error) if error - return callback(new Error("user not found #{user_id} for assignBonus")) if !user? - logger.log user_id: user_id, refered_user_count: user.refered_user_count, "assigning bonus" - if user.refered_user_count? and user.refered_user_count > 0 - newFeatures = ReferalFeatures._calculateFeatures(user) - callback null, newFeatures - else - callback null, {} - - _calculateFeatures : (user)-> - bonusLevel = ReferalFeatures._getBonusLevel(user) - return Settings.bonus_features?["#{bonusLevel}"] or {} - - _getBonusLevel: (user)-> - highestBonusLevel = 0 - _.each _.keys(Settings.bonus_features), (level)-> - levelIsLessThanUser = level <= user.refered_user_count - levelIsMoreThanCurrentHighest = level >= highestBonusLevel - if levelIsLessThanUser and levelIsMoreThanCurrentHighest - highestBonusLevel = level - return highestBonusLevel diff --git a/services/web/app/coffee/Features/Referal/ReferalHandler.coffee b/services/web/app/coffee/Features/Referal/ReferalHandler.coffee deleted file mode 100644 index 6a959adb57..0000000000 --- a/services/web/app/coffee/Features/Referal/ReferalHandler.coffee +++ /dev/null @@ -1,8 +0,0 @@ -User = require('../../models/User').User - -module.exports = - getReferedUsers: (user_id, callback)-> - User.findById user_id, (err, user)-> - refered_users = user.refered_users || [] - refered_user_count= user.refered_user_count || refered_users.length - callback null, refered_users, refered_user_count diff --git a/services/web/app/coffee/Features/References/ReferencesController.coffee b/services/web/app/coffee/Features/References/ReferencesController.coffee deleted file mode 100644 index 305c4715b5..0000000000 --- a/services/web/app/coffee/Features/References/ReferencesController.coffee +++ /dev/null @@ -1,39 +0,0 @@ -logger = require('logger-sharelatex') -ReferencesHandler = require('./ReferencesHandler') -settings = require('settings-sharelatex') -EditorRealTimeController = require("../Editor/EditorRealTimeController") - -module.exports = ReferencesController = - - - index: (req, res) -> - projectId = req.params.Project_id - shouldBroadcast = req.body.shouldBroadcast - docIds = req.body.docIds - if (!docIds or !(docIds instanceof Array)) - logger.err {projectId, docIds}, "docIds is not valid, should be either Array or String 'ALL'" - return res.sendStatus 400 - logger.log {projectId, docIds: docIds}, "index references for project" - ReferencesHandler.index projectId, docIds, (err, data) -> - if err? - logger.err {err, projectId}, "error indexing all references" - return res.sendStatus 500 - ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data) - - indexAll: (req, res) -> - projectId = req.params.Project_id - shouldBroadcast = req.body.shouldBroadcast - logger.log {projectId}, "index all references for project" - ReferencesHandler.indexAll projectId, (err, data) -> - 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 - return res.json data diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee deleted file mode 100644 index 959833351f..0000000000 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ /dev/null @@ -1,108 +0,0 @@ -logger = require("logger-sharelatex") -request = require("request") -settings = require("settings-sharelatex") -ProjectGetter = require "../Project/ProjectGetter" -UserGetter = require "../User/UserGetter" -DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') -_ = require('underscore') -Async = require('async') - -oneMinInMs = 60 * 1000 -fiveMinsInMs = oneMinInMs * 5 - -if !settings.apis?.references?.url? - logger.log "references search not enabled" - -module.exports = ReferencesHandler = - - _buildDocUrl: (projectId, docId) -> - "#{settings.apis.docstore.url}/project/#{projectId}/doc/#{docId}/raw" - - _buildFileUrl: (projectId, fileId) -> - "#{settings.apis.filestore.url}/project/#{projectId}/file/#{fileId}" - - _findBibFileIds: (project) -> - ids = [] - _process = (folder) -> - _.each (folder.fileRefs or []), (file) -> - if file?.name?.match(/^.*\.bib$/) - ids.push(file._id) - _.each (folder.folders or []), (folder) -> - _process(folder) - _.each (project.rootFolder or []), (rootFolder) -> - _process(rootFolder) - return ids - - _findBibDocIds: (project) -> - ids = [] - _process = (folder) -> - _.each (folder.docs or []), (doc) -> - if doc?.name?.match(/^.*\.bib$/) - ids.push(doc._id) - _.each (folder.folders or []), (folder) -> - _process(folder) - _.each (project.rootFolder or []), (rootFolder) -> - _process(rootFolder) - return ids - - _isFullIndex: (project, callback = (err, result) ->) -> - UserGetter.getUser project.owner_ref, { features: true }, (err, owner) -> - return callback(err) if err? - features = owner?.features - callback(null, features?.references == true || features?.referencesSearch == true) - - indexAll: (projectId, callback=(err, data)->) -> - ProjectGetter.getProject projectId, {rootFolder: true, owner_ref: 1}, (err, project) -> - if err - logger.err {err, projectId}, "error finding project" - return callback(err) - logger.log {projectId}, "indexing all bib files in project" - docIds = ReferencesHandler._findBibDocIds(project) - fileIds = ReferencesHandler._findBibFileIds(project) - ReferencesHandler._doIndexOperation(projectId, project, docIds, fileIds, callback) - - index: (projectId, docIds, callback=(err, data)->) -> - ProjectGetter.getProject projectId, {rootFolder: true, owner_ref: 1}, (err, project) -> - if err - logger.err {err, projectId}, "error finding project" - return callback(err) - 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" - return callback(err) - logger.log {projectId, docIds}, 'flushing docs to mongo before calling references service' - Async.series( - docIds.map((docId) -> (cb) -> DocumentUpdaterHandler.flushDocToMongo(projectId, docId, cb)), - (err) -> - # continue - if err - logger.err {err, projectId, docIds}, "error flushing docs to mongo" - return callback(err) - bibDocUrls = docIds.map (docId) -> - ReferencesHandler._buildDocUrl projectId, docId - bibFileUrls = fileIds.map (fileId) -> - ReferencesHandler._buildFileUrl projectId, fileId - allUrls = bibDocUrls.concat(bibFileUrls) - logger.log {projectId, isFullIndex, docIds, bibDocUrls}, "sending request to references service" - request.post { - url: "#{settings.apis.references.url}/project/#{projectId}/index" - json: - docUrls: allUrls - fullIndex: isFullIndex - }, (err, res, data) -> - if err - logger.err {err, projectId}, "error communicating with references api" - return callback(err) - if 200 <= res.statusCode < 300 - logger.log {projectId}, "got keys from references api" - return callback(null, data) - else - err = new Error("references api responded with non-success code: #{res.statusCode}") - logger.log {err, projectId}, "error updating references" - return callback(err) - ) diff --git a/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee b/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee deleted file mode 100644 index 20943628ed..0000000000 --- a/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee +++ /dev/null @@ -1,21 +0,0 @@ -RateLimiter = require('../../infrastructure/RateLimiter') - - -ONE_MIN = 60 -ATTEMPT_LIMIT = 10 - - -module.exports = - - processLoginRequest: (email, callback) -> - opts = - endpointName: 'login' - throttle: ATTEMPT_LIMIT - timeInterval: ONE_MIN * 2 - subjectName: email - RateLimiter.addCount opts, (err, shouldAllow) -> - callback(err, shouldAllow) - - recordSuccessfulLogin: (email, callback = ->)-> - RateLimiter.clearRateLimit 'login', email, callback - diff --git a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee deleted file mode 100644 index 69c9f5b0e9..0000000000 --- a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee +++ /dev/null @@ -1,50 +0,0 @@ -Settings = require('settings-sharelatex') -crypto = require("crypto") -logger = require("logger-sharelatex") -{db} = require "../../infrastructure/mongojs" -Errors = require "../Errors/Errors" - -ONE_HOUR_IN_S = 60 * 60 - -module.exports = - getNewToken: (use, data, options = {}, callback = (error, data) ->)-> - # options is optional - if typeof options == "function" - callback = options - options = {} - expiresIn = options.expiresIn or ONE_HOUR_IN_S - createdAt = new Date() - expiresAt = new Date(createdAt.getTime() + expiresIn * 1000) - token = crypto.randomBytes(32).toString("hex") - logger.log {data, expiresIn, token_start: token.slice(0,8)}, "generating token for #{use}" - db.tokens.insert { - use: use - token: token, - data: data, - createdAt: createdAt, - expiresAt: expiresAt - }, (error) -> - return callback(error) if error? - callback null, token - - getValueFromTokenAndExpire: (use, token, callback = (error, data) ->)-> - logger.log token_start: token.slice(0,8), "getting data from #{use} token" - now = new Date() - db.tokens.findAndModify { - query: { - use: use, - token: token, - expiresAt: { $gt: now }, - usedAt: { $exists: false } - }, - update: { - $set: { - usedAt: now - } - } - }, (error, token) -> - return callback(error) if error? - if !token? - return callback(new Errors.NotFoundError('no token found')) - return callback null, token.data - diff --git a/services/web/app/coffee/Features/Security/RateLimiterMiddleware.coffee b/services/web/app/coffee/Features/Security/RateLimiterMiddleware.coffee deleted file mode 100644 index 631e336cef..0000000000 --- a/services/web/app/coffee/Features/Security/RateLimiterMiddleware.coffee +++ /dev/null @@ -1,41 +0,0 @@ -RateLimiter = require "../../infrastructure/RateLimiter" -logger = require "logger-sharelatex" -AuthenticationController = require('../Authentication/AuthenticationController') - -module.exports = RateLimiterMiddleware = - ### - Do not allow more than opts.maxRequests from a single client in - opts.timeInterval. Pass an array of opts.params to segment this based on - parameters in the request URL, e.g.: - - app.get "/project/:project_id", RateLimiterMiddleware.rateLimit(endpointName: "open-editor", params: ["project_id"]) - - will rate limit each project_id separately. - - Unique clients are identified by user_id if logged in, and IP address if not. - ### - rateLimit: (opts) -> - return (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) || req.ip - params = (opts.params or []).map (p) -> req.params[p] - params.push user_id - subjectName = params.join(":") - if opts.ipOnly - subjectName = req.ip - if !opts.endpointName? - throw new Error("no endpointName provided") - options = { - endpointName: opts.endpointName - timeInterval: opts.timeInterval or 60 - subjectName: subjectName - throttle: opts.maxRequests or 6 - } - RateLimiter.addCount options, (error, canContinue)-> - return next(error) if error? - if canContinue - next() - else - logger.warn options, "rate limit exceeded" - res.status(429) # Too many requests - res.write("Rate limit reached, please try again later") - res.end() diff --git a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee b/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee deleted file mode 100755 index b813878335..0000000000 --- a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee +++ /dev/null @@ -1,90 +0,0 @@ -metrics = require("metrics-sharelatex") -logger = require('logger-sharelatex') -_ = require('underscore') -User = require('../../models/User').User -Project = require('../../models/Project').Project -DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') -Settings = require('settings-sharelatex') -util = require('util') -RecurlyWrapper = require('../Subscription/RecurlyWrapper') -SubscriptionHandler = require('../Subscription/SubscriptionHandler') -projectEntityHandler = require('../Project/ProjectEntityHandler') -TpdsUpdateSender = require("../ThirdPartyDataStore/TpdsUpdateSender") -EditorRealTimeController = require("../Editor/EditorRealTimeController") -SystemMessageManager = require("../SystemMessages/SystemMessageManager") - -oneMinInMs = 60 * 1000 - -updateOpenConnetionsMetrics = ()-> - metrics.gauge "open_connections.socketio", require("../../infrastructure/Server").io?.sockets?.clients()?.length - metrics.gauge "open_connections.http", _.size(require('http').globalAgent?.sockets) - metrics.gauge "open_connections.https", _.size(require('https').globalAgent?.sockets) - setTimeout updateOpenConnetionsMetrics, oneMinInMs - -setTimeout updateOpenConnetionsMetrics, oneMinInMs - - - -module.exports = AdminController = - - index : (req, res, next)=> - http = require('http') - openSockets = {} - for url, agents of require('http').globalAgent.sockets - openSockets["http://#{url}"] = (agent._httpMessage.path for agent in agents) - for url, agents of require('https').globalAgent.sockets - openSockets["https://#{url}"] = (agent._httpMessage.path for agent in agents) - - SystemMessageManager.getMessagesFromDB (error, systemMessages) -> - return next(error) if error? - res.render 'admin/index', - title: 'System Admin' - openSockets: openSockets - systemMessages: systemMessages - - registerNewUser: (req, res, next) -> - res.render 'admin/register' - - dissconectAllUsers: (req, res)=> - logger.warn "disconecting everyone" - EditorRealTimeController.emitToAll 'forceDisconnect', "Sorry, we are performing a quick update to the editor and need to close it down. Please refresh the page to continue." - res.sendStatus(200) - - closeEditor : (req, res)-> - logger.warn "closing editor" - Settings.editorIsOpen = req.body.isOpen - res.sendStatus(200) - - writeAllToMongo : (req, res)-> - logger.log "writing all docs to mongo" - Settings.mongo.writeAll = true - DocumentUpdaterHandler.flushAllDocsToMongo ()-> - logger.log "all docs have been saved to mongo" - res.send() - - syncUserToSubscription: (req, res)-> - {user_id, subscription_id} = req.body - RecurlyWrapper.getSubscription subscription_id, {}, (err, subscription)-> - User.findById user_id, (err, user)-> - SubscriptionHandler.syncSubscriptionToUser subscription, user._id, (err)-> - logger.log user_id:user_id, subscription_id:subscription_id, "linked account to subscription" - res.send() - - flushProjectToTpds: (req, res)-> - projectEntityHandler.flushProjectToThirdPartyDataStore req.body.project_id, (err)-> - res.sendStatus 200 - - pollDropboxForUser: (req, res)-> - user_id = req.body.user_id - TpdsUpdateSender.pollDropboxForUser user_id, () -> - res.sendStatus 200 - - createMessage: (req, res, next) -> - SystemMessageManager.createMessage req.body.content, (error) -> - return next(error) if error? - res.sendStatus 200 - - clearMessages: (req, res, next) -> - SystemMessageManager.clearMessages (error) -> - return next(error) if error? - res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee deleted file mode 100644 index cff53ec171..0000000000 --- a/services/web/app/coffee/Features/Spelling/SpellingController.coffee +++ /dev/null @@ -1,18 +0,0 @@ -request = require 'request' -Settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' -AuthenticationController = require('../Authentication/AuthenticationController') - -TEN_SECONDS = 1000 * 10 - -module.exports = SpellingController = - proxyRequestToSpellingApi: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - url = req.url.slice("/spelling".length) - url = "/user/#{user_id}#{url}" - req.headers["Host"] = Settings.apis.spelling.host - request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS) - .on "error", (error) -> - logger.error err: error, "Spelling API error" - res.status(500).end() - .pipe(res) diff --git a/services/web/app/coffee/Features/StaticPages/HomeController.coffee b/services/web/app/coffee/Features/StaticPages/HomeController.coffee deleted file mode 100755 index a6c0bef978..0000000000 --- a/services/web/app/coffee/Features/StaticPages/HomeController.coffee +++ /dev/null @@ -1,41 +0,0 @@ -logger = require('logger-sharelatex') -Settings = require('settings-sharelatex') -_ = require('underscore') -Features = require "../../infrastructure/Features" - -Path = require "path" -fs = require "fs" - -ErrorController = require "../Errors/ErrorController" -AuthenticationController = require('../Authentication/AuthenticationController') - -slHomepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home/sl.pug") -v2HomepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home/v2.pug") - -module.exports = HomeController = - index : (req,res)-> - if AuthenticationController.isUserLoggedIn(req) - if req.query.scribtex_path? - res.redirect "/project?scribtex_path=#{req.query.scribtex_path}" - else - res.redirect '/project' - else - HomeController.home(req, res) - - home: (req, res, next)-> - if Features.hasFeature('homepage') and !Settings.overleaf and slHomepageExists - res.render 'external/home/sl' - else if Features.hasFeature('homepage') and Settings.overleaf and v2HomepageExists - res.render 'external/home/v2' - else - res.redirect '/login' - - externalPage: (page, title) -> - return (req, res, next = (error) ->) -> - path = Path.resolve(__dirname + "/../../../views/external/#{page}.pug") - fs.exists path, (exists) -> # No error in this callback - old method in Node.js! - if exists - res.render "external/#{page}.pug", - title: title - else - ErrorController.notFound(req, res, next) diff --git a/services/web/app/coffee/Features/StaticPages/StaticPageHelpers.coffee b/services/web/app/coffee/Features/StaticPages/StaticPageHelpers.coffee deleted file mode 100644 index 068fa86e43..0000000000 --- a/services/web/app/coffee/Features/StaticPages/StaticPageHelpers.coffee +++ /dev/null @@ -1,12 +0,0 @@ -extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps", ".gif", ".jpg"] -_ = require("underscore") - -module.exports = - shouldProxy: (url)-> - shouldProxy = _.find extensionsToProxy, (extension)-> - url.indexOf(extension) != -1 - return shouldProxy - - - - diff --git a/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee b/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee deleted file mode 100644 index e33cae555a..0000000000 --- a/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee +++ /dev/null @@ -1,25 +0,0 @@ -HomeController = require('./HomeController') -UniversityController = require("./UniversityController") - - -module.exports = - apply: (webRouter, apiRouter) -> - webRouter.get '/', HomeController.index - webRouter.get '/home', HomeController.home - - webRouter.get '/tos', HomeController.externalPage("tos", "Terms of Service") - webRouter.get '/about', HomeController.externalPage("about", "About Us") - - webRouter.get '/security', HomeController.externalPage("security", "Security") - webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy") - webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance") - webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide") - webRouter.get '/ol-style', HomeController.externalPage("ol_style_guide", "Overleaf Style Guide") - webRouter.get '/jobs', HomeController.externalPage("jobs", "Jobs") - - webRouter.get '/track-changes-and-comments-in-latex', HomeController.externalPage("review-features-page", "Review features") - - webRouter.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX") - - webRouter.get '/university', UniversityController.getIndexPage - webRouter.get '/university/*', UniversityController.getPage diff --git a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee deleted file mode 100644 index 3caa5ecf5b..0000000000 --- a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee +++ /dev/null @@ -1,16 +0,0 @@ -settings = require("settings-sharelatex") -logger = require("logger-sharelatex") -Settings = require("settings-sharelatex") -sixpack = require("../../infrastructure/Sixpack") - - - -module.exports = UniversityController = - - getPage: (req, res, next)-> - url = req.url?.toLowerCase().replace(".html","") - return res.redirect("/i#{url}") - - getIndexPage: (req, res)-> - return res.redirect("/i/university") - diff --git a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee deleted file mode 100644 index d95596b060..0000000000 --- a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee +++ /dev/null @@ -1,102 +0,0 @@ -async = require("async") -PlansLocator = require("./PlansLocator") -_ = require("underscore") -SubscriptionLocator = require("./SubscriptionLocator") -UserFeaturesUpdater = require("./UserFeaturesUpdater") -Settings = require("settings-sharelatex") -logger = require("logger-sharelatex") -ReferalFeatures = require("../Referal/ReferalFeatures") -V1SubscriptionManager = require("./V1SubscriptionManager") -InstitutionsFeatures = require '../Institutions/InstitutionsFeatures' - -oneMonthInSeconds = 60 * 60 * 24 * 30 - -module.exports = FeaturesUpdater = - refreshFeatures: (user_id, notifyV1 = true, callback = (error, features, featuresChanged) ->)-> - if typeof notifyV1 == 'function' - callback = notifyV1 - notifyV1 = true - - if notifyV1 - V1SubscriptionManager.notifyV1OfFeaturesChange user_id, (error) -> - if error? - logger.err {err: error, user_id}, "error notifying v1 about updated features" - - - jobs = - individualFeatures: (cb) -> FeaturesUpdater._getIndividualFeatures user_id, cb - groupFeatureSets: (cb) -> FeaturesUpdater._getGroupFeatureSets user_id, cb - institutionFeatures:(cb) -> InstitutionsFeatures.getInstitutionsFeatures user_id, cb - v1Features: (cb) -> FeaturesUpdater._getV1Features user_id, cb - bonusFeatures: (cb) -> ReferalFeatures.getBonusFeatures user_id, cb - async.series jobs, (err, results)-> - if err? - logger.err err:err, user_id:user_id, - "error getting subscription or group for refreshFeatures" - return callback(err) - - {individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures} = results - logger.log {user_id, individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures}, 'merging user features' - featureSets = groupFeatureSets.concat [individualFeatures, institutionFeatures, v1Features, bonusFeatures] - features = _.reduce(featureSets, FeaturesUpdater._mergeFeatures, Settings.defaultFeatures) - - logger.log {user_id, features}, 'updating user features' - UserFeaturesUpdater.updateFeatures user_id, features, callback - - _getIndividualFeatures: (user_id, callback = (error, features = {}) ->) -> - SubscriptionLocator.getUsersSubscription user_id, (err, sub)-> - callback err, FeaturesUpdater._subscriptionToFeatures(sub) - - _getGroupFeatureSets: (user_id, callback = (error, featureSets = []) ->) -> - SubscriptionLocator.getGroupSubscriptionsMemberOf user_id, (err, subs) -> - callback err, (subs or []).map FeaturesUpdater._subscriptionToFeatures - - _getV1Features: (user_id, callback = (error, features = {}) ->) -> - V1SubscriptionManager.getPlanCodeFromV1 user_id, (err, planCode, v1Id) -> - if err? - return callback(null, []) if err?.name == 'NotFoundError' - return callback(err) - - callback(err, FeaturesUpdater._mergeFeatures( - V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) or {}, - FeaturesUpdater._planCodeToFeatures(planCode) - )) - - _mergeFeatures: (featuresA, featuresB) -> - features = Object.assign({}, featuresA) - for key, value of featuresB - # Special merging logic for non-boolean features - if key == 'compileGroup' - if features['compileGroup'] == 'priority' or featuresB['compileGroup'] == 'priority' - features['compileGroup'] = 'priority' - else - features['compileGroup'] = 'standard' - else if key == 'collaborators' - if features['collaborators'] == -1 or featuresB['collaborators'] == -1 - features['collaborators'] = -1 - else - features['collaborators'] = Math.max( - features['collaborators'] or 0, - featuresB['collaborators'] or 0 - ) - else if key == 'compileTimeout' - features['compileTimeout'] = Math.max( - features['compileTimeout'] or 0, - featuresB['compileTimeout'] or 0 - ) - else - # Boolean keys, true is better - features[key] = features[key] or featuresB[key] - return features - - _subscriptionToFeatures: (subscription) -> - FeaturesUpdater._planCodeToFeatures(subscription?.planCode) - - _planCodeToFeatures: (planCode) -> - if !planCode? - return {} - plan = PlansLocator.findLocalPlanInSettings planCode - if !plan? - return {} - else - return plan.features diff --git a/services/web/app/coffee/Features/Subscription/GroupPlansData.coffee b/services/web/app/coffee/Features/Subscription/GroupPlansData.coffee deleted file mode 100644 index f72f882039..0000000000 --- a/services/web/app/coffee/Features/Subscription/GroupPlansData.coffee +++ /dev/null @@ -1,37 +0,0 @@ -Settings = require 'settings-sharelatex' -fs = require('fs') - -# The groups.json file encodes the various group plan options we provide, and -# is used in the app the render the appropriate dialog in the plans page, and -# to generate the appropriate entries in the Settings.plans array. -# It is also used by scripts/recurly/sync_recurly.rb, which will make sure -# Recurly has a plan configured for all the groups, and that the prices are -# up to date with the data in groups.json. -data = fs.readFileSync(__dirname + '/../../../templates/plans/groups.json') -groups = JSON.parse(data.toString()) - -capitalize = (string) -> - string.charAt(0).toUpperCase() + string.slice(1); - -# With group accounts in Recurly, we end up with a lot of plans to manage. -# Rather than hand coding them in the settings file, and then needing to keep -# that data in sync with the data in groups.json, we can auto generate the -# group plan entries and append them to Settings.plans at boot time. This is not -# a particularly clean pattern, since it's a little surprising that settings -# are modified at boot-time, but I think it's a better option than trying to -# keep two sources of data in sync. -for usage, plan_data of groups - for plan_code, currency_data of plan_data - for currency, price_data of currency_data - for size, price of price_data - Settings.plans.push { - planCode: "group_#{plan_code}_#{size}_#{usage}", - name: "#{Settings.appName} #{capitalize(plan_code)} - Group Account (#{size} licenses) - #{capitalize(usage)}", - hideFromUsers: true, - annual: true - features: Settings.features[plan_code] - groupPlan: true - membersLimit: parseInt(size) - } - -module.exports = groups diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee deleted file mode 100644 index c34b6c028d..0000000000 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ /dev/null @@ -1,97 +0,0 @@ -logger = require("logger-sharelatex") -ProjectGetter = require('../Project/ProjectGetter') -UserGetter = require("../User/UserGetter") -SubscriptionLocator = require("./SubscriptionLocator") -Settings = require("settings-sharelatex") -CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") -CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler") -V1SubscriptionManager = require("./V1SubscriptionManager") - -module.exports = LimitationsManager = - allowedNumberOfCollaboratorsInProject: (project_id, callback) -> - ProjectGetter.getProject project_id, owner_ref: true, (error, project) => - return callback(error) if error? - @allowedNumberOfCollaboratorsForUser project.owner_ref, callback - - allowedNumberOfCollaboratorsForUser: (user_id, callback) -> - UserGetter.getUser user_id, {features: 1}, (error, user) -> - return callback(error) if error? - if user.features? and user.features.collaborators? - callback null, user.features.collaborators - else - callback null, Settings.defaultFeatures.collaborators - - canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) -> - @allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) => - return callback(error) if error? - CollaboratorsHandler.getInvitedCollaboratorCount project_id, (error, current_number) => - return callback(error) if error? - CollaboratorsInvitesHandler.getInviteCount project_id, (error, invite_count) => - return callback(error) if error? - if current_number + invite_count + x_collaborators <= allowed_number or allowed_number < 0 - callback null, true - else - callback null, false - - hasPaidSubscription: (user, callback = (err, hasSubscriptionOrIsMember)->) -> - @userHasV2Subscription user, (err, hasSubscription, subscription)=> - return callback(err) if err? - @userIsMemberOfGroupSubscription user, (err, isMember)=> - return callback(err) if err? - @userHasV1Subscription user, (err, hasV1Subscription)=> - return callback(err) if err? - logger.log {user_id:user._id, isMember, hasSubscription, hasV1Subscription}, "checking if user has subscription or is group member" - callback err, isMember or hasSubscription or hasV1Subscription, subscription - - - # alias for backward-compatibility with modules. Use `haspaidsubscription` instead - userHasSubscriptionOrIsGroupMember: (user, callback) -> - @hasPaidSubscription(user, callback) - - userHasV2Subscription: (user, callback = (err, hasSubscription, subscription)->) -> - logger.log user_id:user._id, "checking if user has subscription" - SubscriptionLocator.getUsersSubscription user._id, (err, subscription)-> - if err? - return callback(err) - 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 - - userHasV1OrV2Subscription: (user, callback = (err, hasSubscription) ->) -> - @userHasV2Subscription user, (err, hasV2Subscription) => - return callback(err) if err? - return callback null, true if hasV2Subscription - @userHasV1Subscription user, (err, hasV1Subscription) => - return callback(err) if err? - return callback null, true if hasV1Subscription - return callback null, false - - userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) -> - 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 - - userHasV1Subscription: (user, callback = (error, hasV1Subscription) ->) -> - V1SubscriptionManager.getSubscriptionsFromV1 user._id, (err, v1Subscription) -> - logger.log {user_id: user._id, v1Subscription}, '[userHasV1Subscription]' - callback err, !!v1Subscription?.has_subscription - - teamHasReachedMemberLimit: (subscription) -> - currentTotal = (subscription.member_ids or []).length + - (subscription.teamInvites or []).length + - (subscription.invited_emails or []).length - - return currentTotal >= subscription.membersLimit - - hasGroupMembersLimitReached: (subscriptionId, callback = (err, limitReached, subscription)->)-> - SubscriptionLocator.getSubscription subscriptionId, (err, subscription)-> - if err? - logger.err err:err, subscriptionId: subscriptionId, "error getting subscription" - return callback(err) - if !subscription? - logger.err subscriptionId: subscriptionId, "no subscription found" - return callback("no subscription found") - - limitReached = LimitationsManager.teamHasReachedMemberLimit(subscription) - callback(err, limitReached, subscription) diff --git a/services/web/app/coffee/Features/Subscription/PlansLocator.coffee b/services/web/app/coffee/Features/Subscription/PlansLocator.coffee deleted file mode 100644 index 49d7a29b79..0000000000 --- a/services/web/app/coffee/Features/Subscription/PlansLocator.coffee +++ /dev/null @@ -1,9 +0,0 @@ -Settings = require("settings-sharelatex") - -module.exports = - - findLocalPlanInSettings: (planCode) -> - for plan in Settings.plans - return plan if plan.planCode == planCode - return null - diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee deleted file mode 100644 index 0a179cd81f..0000000000 --- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee +++ /dev/null @@ -1,540 +0,0 @@ -querystring = require 'querystring' -crypto = require 'crypto' -request = require 'request' -Settings = require "settings-sharelatex" -xml2js = require "xml2js" -logger = require("logger-sharelatex") -Async = require('async') - -module.exports = RecurlyWrapper = - apiUrl : Settings.apis?.recurly?.url or "https://api.recurly.com/v2" - - _addressToXml: (address) -> - allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code'] - resultString = "\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 || ''}\n" - resultString += "\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" - expect404: true - }, (error, response, responseBody) -> - if error - logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account" - return next(error) - if response.statusCode == 404 # actually not an error in this case, just no existing account - logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed" - cache.userExists = false - return next(null, cache) - 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 = """ - - #{user._id} - #{user.email} - #{user.first_name} - #{user.last_name} -
- #{address.address1} - #{address.address2} - #{address.city || ''} - #{address.state || ''} - #{address.zip || ''} - #{address.country} -
-
- """ - 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 = """ - - #{recurly_token_id} - - """ - 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 = """ - - #{subscriptionDetails.plan_code} - #{subscriptionDetails.currencyCode} - #{subscriptionDetails.coupon_code} - - #{user._id} - - - """ # 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 = """ - - #{subscriptionDetails.plan_code} - #{subscriptionDetails.currencyCode} - #{subscriptionDetails.coupon_code} - - #{user._id} - #{user.email} - #{user.first_name} - #{user.last_name} - - #{recurly_token_id} - - - - """ - RecurlyWrapper.apiRequest({ - url : "subscriptions" - method : "POST" - body : requestBody - }, (error, response, responseBody) => - return callback(error) if error? - 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 = RecurlyWrapper.apiUrl + "/" + options.url - options.headers = - "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64") - "Accept" : "application/xml" - "Content-Type" : "application/xml; charset=utf-8" - expect404 = options.expect404 - delete options.expect404 - request options, (error, response, body) -> - unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404) - logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly" - error = "Recurly API returned with status code: #{response.statusCode}" - if response.statusCode == 404 and expect404 - logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response" - callback(error, response, body) - - getSubscriptions: (accountId, callback)-> - RecurlyWrapper.apiRequest({ - url: "accounts/#{accountId}/subscriptions" - }, (error, response, body) => - return callback(error) if error? - RecurlyWrapper._parseXml body, callback - ) - - - getSubscription: (subscriptionId, options, callback) -> - callback = options unless callback? - options ||= {} - - if options.recurlyJsResult - url = "recurly_js/result/#{subscriptionId}" - else - url = "subscriptions/#{subscriptionId}" - - RecurlyWrapper.apiRequest({ - url: url - }, (error, response, body) => - return callback(error) if error? - RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) => - return callback(error) if error? - if options.includeAccount - if recurlySubscription.account? and recurlySubscription.account.url? - accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1] - else - return callback "I don't understand the response from Recurly" - - RecurlyWrapper.getAccount accountId, (error, account) -> - return callback(error) if error? - recurlySubscription.account = account - callback null, recurlySubscription - - else - callback null, recurlySubscription - ) - - getAccounts: (callback)-> - allAccounts = [] - getPageOfAccounts = (cursor = null)=> - opts = - url: "accounts" - qs: - per_page:200 - if cursor? - opts.qs.cursor = cursor - RecurlyWrapper.apiRequest opts, (error, response, body) => - return callback(error) if error? - RecurlyWrapper._parseXml body, (err, data)-> - if err? - logger.err err:err, "could not get accoutns" - callback(err) - allAccounts = allAccounts.concat(data.accounts) - logger.log "got another #{data.accounts.length}, total now #{allAccounts.length}" - cursor = response.headers.link?.match(/cursor=([0-9]+)&/)?[1] - if cursor? - getPageOfAccounts(cursor) - else - callback(err, allAccounts) - - getPageOfAccounts() - - - getAccount: (accountId, callback) -> - RecurlyWrapper.apiRequest({ - url: "accounts/#{accountId}" - }, (error, response, body) => - return callback(error) if error? - RecurlyWrapper._parseAccountXml body, callback - ) - - getAccountActiveCoupons: (accountId, callback) -> - RecurlyWrapper.apiRequest({ - url: "accounts/#{accountId}/redemptions" - }, (error, response, body) => - return callback(error) if error? - RecurlyWrapper._parseRedemptionsXml body, (error, redemptions) -> - return callback(error) if error? - activeRedemptions = redemptions.filter (redemption) -> - redemption.state == 'active' - couponCodes = activeRedemptions.map (redemption) -> - redemption.coupon_code - Async.map couponCodes, RecurlyWrapper.getCoupon, (error, coupons) -> - return callback(error) if error? - callback(null, coupons) - ) - - getCoupon: (couponCode, callback) -> - opts = { url: "coupons/#{couponCode}" } - RecurlyWrapper.apiRequest opts, (error, response, body) -> - RecurlyWrapper._parseCouponXml body, callback - - getBillingInfo: (accountId, callback)-> - RecurlyWrapper.apiRequest({ - url: "accounts/#{accountId}/billing_info" - }, (error, response, body) => - return callback(error) if error? - RecurlyWrapper._parseXml body, callback - ) - - - updateSubscription: (subscriptionId, options, callback) -> - logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription" - requestBody = """ - - #{options.plan_code} - #{options.timeframe} - - """ - RecurlyWrapper.apiRequest({ - url : "subscriptions/#{subscriptionId}" - method : "put" - body : requestBody - }, (error, response, responseBody) => - return callback(error) if error? - RecurlyWrapper._parseSubscriptionXml responseBody, callback - ) - - createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)-> - requestBody = """ - - #{coupon_code} - #{name} - dollars - - <#{currencyCode}>#{discount_in_cents} - - - #{plan_code} - - false - - """ - logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon" - RecurlyWrapper.apiRequest({ - url : "coupons" - method : "post" - body : requestBody - }, (error, response, responseBody) => - if error? - logger.err err:error, coupon_code:coupon_code, "error creating coupon" - callback(error) - ) - - - lookupCoupon: (coupon_code, callback)-> - RecurlyWrapper.apiRequest({ - url: "coupons/#{coupon_code}" - }, (error, response, body) => - return callback(error) if error? - RecurlyWrapper._parseXml body, callback - ) - - cancelSubscription: (subscriptionId, callback) -> - logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription" - RecurlyWrapper.apiRequest({ - url: "subscriptions/#{subscriptionId}/cancel", - method: "put" - }, (error, response, body) -> - if error? - RecurlyWrapper._parseXml body, (_err, parsed) -> - if parsed?.error?.description == "A canceled subscription can't transition to canceled" - logger.log {subscriptionId, error, body}, "subscription already cancelled, not really an error, proceeding" - callback(null) - else - callback(error) - else - callback(null) - ) - - reactivateSubscription: (subscriptionId, callback) -> - logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription" - RecurlyWrapper.apiRequest({ - url: "subscriptions/#{subscriptionId}/reactivate", - method: "put" - }, (error, response, body) -> - callback(error) - ) - - - redeemCoupon: (account_code, coupon_code, callback)-> - requestBody = """ - - #{account_code} - USD - - """ - logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user" - RecurlyWrapper.apiRequest({ - url : "coupons/#{coupon_code}/redeem" - method : "post" - body : requestBody - }, (error, response, responseBody) => - if error? - logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon" - callback(error) - ) - - extendTrial: (subscriptionId, daysUntilExpire = 7, callback)-> - 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" - RecurlyWrapper.apiRequest({ - url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false" - method : "put" - }, (error, response, responseBody) => - if error? - logger.err err:error, subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "error exending trial" - callback(error) - ) - - listAccountActiveSubscriptions: (account_id, callback = (error, subscriptions) ->) -> - RecurlyWrapper.apiRequest { - url: "accounts/#{account_id}/subscriptions" - qs: - state: "active" - expect404: true - }, (error, response, body) -> - return callback(error) if error? - if response.statusCode == 404 - return callback null, [] - else - RecurlyWrapper._parseSubscriptionsXml body, callback - - _parseSubscriptionsXml: (xml, callback) -> - RecurlyWrapper._parseXmlAndGetAttribute xml, "subscriptions", callback - - _parseSubscriptionXml: (xml, callback) -> - RecurlyWrapper._parseXmlAndGetAttribute xml, "subscription", callback - - _parseAccountXml: (xml, callback) -> - RecurlyWrapper._parseXmlAndGetAttribute xml, "account", callback - - _parseBillingInfoXml: (xml, callback) -> - RecurlyWrapper._parseXmlAndGetAttribute xml, "billing_info", callback - - _parseRedemptionsXml: (xml, callback) -> - RecurlyWrapper._parseXmlAndGetAttribute xml, "redemptions", callback - - _parseCouponXml: (xml, callback) -> - RecurlyWrapper._parseXmlAndGetAttribute xml, "coupon", callback - - _parseXmlAndGetAttribute: (xml, attribute, callback) -> - RecurlyWrapper._parseXml xml, (error, data) -> - return callback(error) if error? - if data? and data[attribute]? - return callback null, data[attribute] - else - return callback(new Error("I don't understand the response from Recurly")) - - _parseXml: (xml, callback) -> - convertDataTypes = (data) -> - if data? and data["$"]? - if data["$"]["nil"] == "nil" - data = null - else if data["$"].href? - data.url = data["$"].href - delete data["$"] - else if data["$"]["type"] == "integer" - data = parseInt(data["_"], 10) - else if data["$"]["type"] == "datetime" - data = new Date(data["_"]) - else if data["$"]["type"] == "array" - delete data["$"] - array = [] - for key, value of data - if value instanceof Array - array = array.concat(convertDataTypes(value)) - else - array.push(convertDataTypes(value)) - data = array - - if data instanceof Array - data = (convertDataTypes(entry) for entry in data) - else if typeof data == "object" - for key, value of data - data[key] = convertDataTypes(value) - return data - - parser = new xml2js.Parser( - explicitRoot : true - explicitArray : false - emptyTag: '' - ) - parser.parseString xml, (error, data) -> - return callback(error) if error? - result = convertDataTypes(data) - callback null, result diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee deleted file mode 100644 index 6edd79b94f..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ /dev/null @@ -1,269 +0,0 @@ -AuthenticationController = require '../Authentication/AuthenticationController' -SubscriptionHandler = require './SubscriptionHandler' -PlansLocator = require("./PlansLocator") -SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder') -LimitationsManager = require("./LimitationsManager") -RecurlyWrapper = require './RecurlyWrapper' -Settings = require 'settings-sharelatex' -logger = require('logger-sharelatex') -GeoIpLookup = require("../../infrastructure/GeoIpLookup") -UserGetter = require "../User/UserGetter" -FeaturesUpdater = require './FeaturesUpdater' -planFeatures = require './planFeatures' -GroupPlansData = require './GroupPlansData' -V1SubscriptionManager = require "./V1SubscriptionManager" - -module.exports = SubscriptionController = - - plansPage: (req, res, next) -> - plans = SubscriptionViewModelBuilder.buildViewModel() - viewName = "subscriptions/plans" - if req.query.v? - viewName = "#{viewName}_#{req.query.v}" - logger.log viewName:viewName, "showing plans page" - currentUser = null - - GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)-> - return next(err) if err? - render = () -> - res.render viewName, - title: "plans_and_pricing" - plans: plans - gaExperiments: Settings.gaExperiments.plansPage - recomendedCurrency:recomendedCurrency - planFeatures: planFeatures - groupPlans: GroupPlansData - user_id = AuthenticationController.getLoggedInUserId(req) - if user_id? - UserGetter.getUser user_id, {signUpDate: 1}, (err, user) -> - return next(err) if err? - currentUser = user - render() - else - render() - - #get to show the recurly.js page - paymentPage: (req, res, next) -> - user = AuthenticationController.getSessionUser(req) - plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) - LimitationsManager.userHasV1OrV2Subscription user, (err, hasSubscription)-> - return next(err) if err? - if hasSubscription or !plan? - res.redirect "/user/subscription?hasSubscription=true" - else - # LimitationsManager.userHasV2Subscription only checks Mongo. Double check with - # Recurly as well at this point (we don't do this most places for speed). - SubscriptionHandler.validateNoSubscriptionInRecurly user._id, (error, valid) -> - return next(error) if error? - if !valid - res.redirect "/user/subscription?hasSubscription=true" - return - else - currency = req.query.currency?.toUpperCase() - GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)-> - return next(err) if err? - if recomendedCurrency? and !currency? - currency = recomendedCurrency - res.render "subscriptions/new", - title : "subscribe" - plan_code: req.query.planCode - currency: currency - countryCode:countryCode - plan:plan - showStudentPlan: req.query.ssp - recurlyConfig: JSON.stringify - currency: currency - subdomain: Settings.apis.recurly.subdomain - showCouponField: req.query.scf - showVatField: req.query.svf - couponCode: req.query.cc or "" - - - - userSubscriptionPage: (req, res, next) -> - user = AuthenticationController.getSessionUser(req) - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, results) -> - return next(error) if error? - { - personalSubscription, - memberGroupSubscriptions, - managedGroupSubscriptions, - confirmedMemberInstitutions, - managedInstitutions, - managedPublishers, - v1SubscriptionStatus - } = results - LimitationsManager.userHasV1OrV2Subscription user, (err, hasSubscription) -> - return next(error) if error? - fromPlansPage = req.query.hasSubscription - logger.log { - user, - hasSubscription, - fromPlansPage, - personalSubscription, - memberGroupSubscriptions, - managedGroupSubscriptions, - confirmedMemberInstitutions, - managedInstitutions, - managedPublishers, - v1SubscriptionStatus - }, "showing subscription dashboard" - plans = SubscriptionViewModelBuilder.buildViewModel() - data = { - title: "your_subscription" - plans, - user, - hasSubscription, - fromPlansPage, - personalSubscription, - memberGroupSubscriptions, - managedGroupSubscriptions, - confirmedMemberInstitutions, - managedInstitutions, - managedPublishers, - v1SubscriptionStatus - } - res.render "subscriptions/dashboard", data - - createSubscription: (req, res, next)-> - user = AuthenticationController.getSessionUser(req) - recurly_token_id = req.body.recurly_token_id - subscriptionDetails = req.body.subscriptionDetails - logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription" - - LimitationsManager.userHasV1OrV2Subscription user, (err, hasSubscription)-> - return next(err) if err? - if hasSubscription - logger.warn {user_id: user._id}, 'user already has subscription' - res.sendStatus 409 # conflict - SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong creating subscription" - return next(err) - res.sendStatus 201 - - successful_subscription: (req, res, next)-> - user = AuthenticationController.getSessionUser(req) - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, {personalSubscription}) -> - return next(error) if error? - if !personalSubscription? - return res.redirect '/user/subscription/plans' - res.render "subscriptions/successful_subscription", - title: "thank_you" - personalSubscription: personalSubscription - - cancelSubscription: (req, res, next) -> - user = AuthenticationController.getSessionUser(req) - logger.log user_id:user._id, "canceling subscription" - SubscriptionHandler.cancelSubscription user, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong canceling subscription" - return next(err) - # Note: this redirect isn't used in the main flow as the redirection is - # handled by Angular - res.redirect "/user/subscription/canceled" - - canceledSubscription: (req, res, next)-> - user = AuthenticationController.getSessionUser(req) - res.render "subscriptions/canceled_subscription", - title: "subscription_canceled" - - cancelV1Subscription: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log {user_id}, "canceling v1 subscription" - V1SubscriptionManager.cancelV1Subscription user_id, (err)-> - if err? - logger.err err:err, user_id:user_id, "something went wrong canceling v1 subscription" - return next(err) - res.redirect "/user/subscription" - - updateSubscription: (req, res, next)-> - _origin = req?.query?.origin || null - user = AuthenticationController.getSessionUser(req) - planCode = req.body.plan_code - if !planCode? - err = new Error('plan_code is not defined') - logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form" - return next(err) - logger.log planCode: planCode, user_id:user._id, "updating subscription" - SubscriptionHandler.updateSubscription user, planCode, null, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong updating subscription" - return next(err) - res.redirect "/user/subscription" - - reactivateSubscription: (req, res, next)-> - user = AuthenticationController.getSessionUser(req) - logger.log user_id:user._id, "reactivating subscription" - SubscriptionHandler.reactivateSubscription user, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong reactivating subscription" - return next(err) - res.redirect "/user/subscription" - - recurlyCallback: (req, res, next)-> - logger.log data: req.body, "received recurly callback" - # we only care if a subscription has exipired - if req.body? and req.body["expired_subscription_notification"]? - recurlySubscription = req.body["expired_subscription_notification"].subscription - SubscriptionHandler.recurlyCallback recurlySubscription, (err)-> - return next(err) if err? - res.sendStatus 200 - else - res.sendStatus 200 - - renderUpgradeToAnnualPlanPage: (req, res, next)-> - user = AuthenticationController.getSessionUser(req) - LimitationsManager.userHasV2Subscription user, (err, hasSubscription, subscription)-> - return next(err) if err? - planCode = subscription?.planCode.toLowerCase() - if planCode?.indexOf("annual") != -1 - planName = "annual" - else if planCode?.indexOf("student") != -1 - planName = "student" - else if planCode?.indexOf("collaborator") != -1 - planName = "collaborator" - if !hasSubscription - return res.redirect("/user/subscription/plans") - logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page" - res.render "subscriptions/upgradeToAnnual", - title: "Upgrade to annual" - planName: planName - - processUpgradeToAnnualPlan: (req, res, next)-> - user = AuthenticationController.getSessionUser(req) - {planName} = req.body - coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName] - annualPlanName = "#{planName}-annual" - logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount" - SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)-> - if err? - logger.err err:err, user_id:user._id, "error updating subscription" - return next(err) - res.sendStatus 200 - - extendTrial: (req, res, next)-> - user = AuthenticationController.getSessionUser(req) - LimitationsManager.userHasV2Subscription user, (err, hasSubscription, subscription)-> - return next(err) if err? - SubscriptionHandler.extendTrial subscription, 14, (err)-> - if err? - res.send 500 - else - res.send 200 - - recurlyNotificationParser: (req, res, next) -> - xml = "" - req.on "data", (chunk) -> - xml += chunk - req.on "end", () -> - RecurlyWrapper._parseXml xml, (error, body) -> - return next(error) if error? - req.body = body - next() - - refreshUserFeatures: (req, res, next) -> - {user_id} = req.params - FeaturesUpdater.refreshFeatures user_id, (error) -> - return next(error) if error? - res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee deleted file mode 100644 index f0929a08cf..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee +++ /dev/null @@ -1,33 +0,0 @@ -dateformat = require 'dateformat' -settings = require "settings-sharelatex" - - -currenySymbols = - EUR: "€" - USD: "$" - GBP: "£" - SEK: "kr" - CAD: "$" - NOK: "kr" - DKK: "kr" - AUD: "$" - NZD: "$" - CHF: "Fr" - SGD: "$" - - -module.exports = - - formatPrice: (priceInCents, currency = "USD") -> - string = priceInCents + "" - string = "0" + string if string.length == 2 - string = "00" + string if string.length == 1 - string = "000" if string.length == 0 - cents = string.slice(-2) - dollars = string.slice(0, -2) - symbol = currenySymbols[currency] - return "#{symbol}#{dollars}.#{cents}" - - formatDate: (date) -> - return null if !date? - dateformat date, "dS mmmm yyyy" \ No newline at end of file diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee deleted file mode 100644 index 6fb58a7cd9..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ /dev/null @@ -1,48 +0,0 @@ -SubscriptionGroupHandler = require("./SubscriptionGroupHandler") -logger = require("logger-sharelatex") -SubscriptionLocator = require("./SubscriptionLocator") -AuthenticationController = require('../Authentication/AuthenticationController') -_ = require("underscore") -async = require("async") - -module.exports = - - removeUserFromGroup: (req, res, next)-> - subscription = req.entity - userToRemove_id = req.params.user_id - logger.log subscriptionId: subscription._id, userToRemove_id:userToRemove_id, "removing user from group subscription" - SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)-> - if err? - logger.err err:err, subscriptionId: subscription._id, userToRemove_id:userToRemove_id, "error removing user from group" - return next(err) - res.send() - - removeSelfFromGroup: (req, res, next)-> - adminUserId = req.query.admin_user_id - userToRemove_id = AuthenticationController.getLoggedInUserId(req) - getManagedSubscription adminUserId, (error, subscription) -> - return next(error) if error? - logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request" - SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)-> - if err? - logger.err err:err, userToRemove_id:userToRemove_id, adminUserId:adminUserId, "error removing self from group" - return res.sendStatus 500 - res.send() - - # legacy route - redirectToSubscriptionGroupAdminPage: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - getManagedSubscription user_id, (error, subscription) -> - return next(error) if error? - if !subscription?.groupPlan - return res.redirect("/user/subscription") - res.redirect("/manage/groups/#{subscription._id}/members") - -getManagedSubscription = (managerId, callback) -> - SubscriptionLocator.findManagedSubscription managerId, (err, subscription)-> - if subscription? - logger.log managerId: managerId, "got managed subscription" - else - err ||= new Error("No subscription found managed by user #{managerId}") - - return callback(err, subscription) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee deleted file mode 100644 index e378cd9c56..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ /dev/null @@ -1,59 +0,0 @@ -async = require("async") -_ = require("underscore") -SubscriptionUpdater = require("./SubscriptionUpdater") -SubscriptionLocator = require("./SubscriptionLocator") -UserGetter = require("../User/UserGetter") -Subscription = require("../../models/Subscription").Subscription -LimitationsManager = require("./LimitationsManager") -logger = require("logger-sharelatex") -OneTimeTokenHandler = require("../Security/OneTimeTokenHandler") -EmailHandler = require("../Email/EmailHandler") -settings = require("settings-sharelatex") -NotificationsBuilder = require("../Notifications/NotificationsBuilder") -UserMembershipViewModel = require("../UserMembership/UserMembershipViewModel") - -module.exports = SubscriptionGroupHandler = - - removeUserFromGroup: (subscriptionId, userToRemove_id, callback)-> - SubscriptionUpdater.removeUserFromGroup subscriptionId, userToRemove_id, callback - - replaceUserReferencesInGroups: (oldId, newId, callback) -> - logger.log old_id: oldId, new_id: newId, "replacing user reference in groups" - Subscription.update {admin_id: oldId}, {admin_id: newId}, (error) -> - return callback(error) if error? - - replaceInArray Subscription, "manager_ids", oldId, newId, (error) -> - return callback(error) if error? - - replaceInArray Subscription, "member_ids", oldId, newId, callback - - isUserPartOfGroup: (user_id, subscription_id, callback=(err, partOfGroup)->)-> - SubscriptionLocator.getSubscriptionByMemberIdAndId user_id, subscription_id, (err, subscription)-> - if subscription? - partOfGroup = true - else - partOfGroup = false - logger.log user_id:user_id, subscription_id:subscription_id, partOfGroup:partOfGroup, "checking if user is part of a group" - callback(err, partOfGroup) - - getTotalConfirmedUsersInGroup: (subscription_id, callback=(err, totalUsers)->)-> - SubscriptionLocator.getSubscription subscription_id, (err, subscription)-> - callback(err, subscription?.member_ids?.length) - -replaceInArray = (model, property, oldValue, newValue, callback) -> - logger.log "Replacing #{oldValue} with #{newValue} in #{property} of #{model}" - - # Mongo won't let us pull and addToSet in the same query, so do it in - # two. Note we need to add first, since the query is based on the old user. - query = {} - query[property] = oldValue - - setNewValue = {} - setNewValue[property] = newValue - - setOldValue = {} - setOldValue[property] = oldValue - - model.update query, { $addToSet: setNewValue }, { multi: true }, (error) -> - return callback(error) if error? - model.update query, { $pull: setOldValue }, { multi: true }, callback diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee deleted file mode 100644 index a57916fac7..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee +++ /dev/null @@ -1,96 +0,0 @@ -async = require("async") -RecurlyWrapper = require("./RecurlyWrapper") -Settings = require "settings-sharelatex" -User = require('../../models/User').User -logger = require('logger-sharelatex') -SubscriptionUpdater = require("./SubscriptionUpdater") -LimitationsManager = require('./LimitationsManager') -EmailHandler = require("../Email/EmailHandler") -Events = require "../../infrastructure/Events" -Analytics = require("../Analytics/AnalyticsManager") - - -module.exports = - validateNoSubscriptionInRecurly: (user_id, callback = (error, valid) ->) -> - RecurlyWrapper.listAccountActiveSubscriptions user_id, (error, subscriptions = []) -> - return callback(error) if error? - if subscriptions.length > 0 - SubscriptionUpdater.syncSubscription subscriptions[0], user_id, (error) -> - return callback(error) if error? - return callback(null, false) - else - return callback(null, true) - - createSubscription: (user, subscriptionDetails, recurly_token_id, callback)-> - self = @ - clientTokenId = "" - @validateNoSubscriptionInRecurly user._id, (error, valid) -> - return callback(error) if error? - if !valid - return callback(new Error("user already has subscription in recurly")) - RecurlyWrapper.createSubscription user, subscriptionDetails, recurly_token_id, (error, recurlySubscription)-> - return callback(error) if error? - SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) -> - return callback(error) if error? - callback() - - updateSubscription: (user, plan_code, coupon_code, callback)-> - logger.log user:user, plan_code:plan_code, coupon_code:coupon_code, "updating subscription" - LimitationsManager.userHasV2Subscription user, (err, hasSubscription, subscription)-> - if !hasSubscription - return callback() - else - async.series [ - (cb)-> - return cb() if !coupon_code? - logger.log user_id:user._id, plan_code:plan_code, coupon_code:coupon_code, "updating subscription with coupon code applied first" - RecurlyWrapper.getSubscription subscription.recurlySubscription_id, includeAccount: true, (err, usersSubscription)-> - return callback(err) if err? - account_code = usersSubscription.account.account_code - RecurlyWrapper.redeemCoupon account_code, coupon_code, cb - (cb)-> - RecurlyWrapper.updateSubscription subscription.recurlySubscription_id, {plan_code: plan_code, timeframe: "now"}, (error, recurlySubscription) -> - return callback(error) if error? - SubscriptionUpdater.syncSubscription recurlySubscription, user._id, cb - ], callback - - - cancelSubscription: (user, callback) -> - LimitationsManager.userHasV2Subscription user, (err, hasSubscription, subscription)-> - if hasSubscription - RecurlyWrapper.cancelSubscription subscription.recurlySubscription_id, (error) -> - return callback(error) if error? - emailOpts = - to: user.email - first_name: user.first_name - ONE_HOUR_IN_MS = 1000 * 60 * 60 - setTimeout (-> EmailHandler.sendEmail "canceledSubscription", emailOpts - ), ONE_HOUR_IN_MS - Events.emit "cancelSubscription", user._id - Analytics.recordEvent user._id, "subscription-canceled" - callback() - else - callback() - - reactivateSubscription: (user, callback) -> - LimitationsManager.userHasV2Subscription user, (err, hasSubscription, subscription)-> - if hasSubscription - RecurlyWrapper.reactivateSubscription subscription.recurlySubscription_id, (error) -> - return callback(error) if error? - EmailHandler.sendEmail "reactivatedSubscription", to: user.email - Analytics.recordEvent user._id, "subscription-reactivated" - callback() - else - callback() - - recurlyCallback: (recurlySubscription, callback) -> - RecurlyWrapper.getSubscription recurlySubscription.uuid, includeAccount: true, (error, recurlySubscription) -> - return callback(error) if error? - User.findById recurlySubscription.account.account_code, (error, user) -> - return callback(error) if error? - if !user? - return callback("no user found") - SubscriptionUpdater.syncSubscription recurlySubscription, user?._id, callback - - extendTrial: (subscription, daysToExend, callback)-> - RecurlyWrapper.extendTrial subscription.recurlySubscription_id, daysToExend, callback diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee deleted file mode 100644 index 62064b602c..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ /dev/null @@ -1,49 +0,0 @@ -Subscription = require('../../models/Subscription').Subscription -logger = require("logger-sharelatex") -ObjectId = require('mongoose').Types.ObjectId - -module.exports = SubscriptionLocator = - - getUsersSubscription: (user_or_id, callback)-> - user_id = @_getUserId(user_or_id) - logger.log user_id:user_id, "getting users subscription" - Subscription.findOne admin_id:user_id, (err, subscription)-> - logger.log user_id:user_id, "got users subscription" - callback(err, subscription) - - findManagedSubscription: (managerId, callback)-> - logger.log managerId: managerId, "finding managed subscription" - Subscription.findOne manager_ids: managerId, callback - - getManagedGroupSubscriptions: (user_or_id, callback = (error, managedSubscriptions) ->) -> - user_id = @_getUserId(user_or_id) - Subscription.find({ - manager_ids: user_or_id, - groupPlan: true - }).populate("admin_id").exec callback - - getMemberSubscriptions: (user_or_id, callback) -> - user_id = @_getUserId(user_or_id) - logger.log user_id: user_id, "getting users group subscriptions" - Subscription.find(member_ids: user_id).populate("admin_id").exec callback - - getSubscription: (subscription_id, callback)-> - Subscription.findOne _id:subscription_id, callback - - getSubscriptionByMemberIdAndId: (user_id, subscription_id, callback)-> - Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback - - getGroupSubscriptionsMemberOf: (user_id, callback)-> - Subscription.find {member_ids: user_id}, {_id:1, planCode:1}, callback - - getGroupsWithEmailInvite: (email, callback) -> - Subscription.find { invited_emails: email }, callback - - getGroupWithV1Id: (v1TeamId, callback) -> - Subscription.findOne { "overleaf.id": v1TeamId }, callback - - _getUserId: (user_or_id) -> - if user_or_id? and user_or_id._id? - return user_or_id._id - else if user_or_id? - return user_or_id diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee deleted file mode 100644 index 57c1dba8c8..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ /dev/null @@ -1,55 +0,0 @@ -AuthenticationController = require('../Authentication/AuthenticationController') -SubscriptionController = require('./SubscriptionController') -SubscriptionGroupController = require './SubscriptionGroupController' -TeamInvitesController = require './TeamInvitesController' -RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') -Settings = require "settings-sharelatex" - -module.exports = - apply: (webRouter, privateApiRouter, publicApiRouter) -> - return unless Settings.enableSubscriptions - - webRouter.get '/user/subscription/plans', SubscriptionController.plansPage - - webRouter.get '/user/subscription', AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage - - webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage - - webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription - - webRouter.get '/user/subscription/canceled', AuthenticationController.requireLogin(), SubscriptionController.canceledSubscription - - - webRouter.get '/subscription/group', AuthenticationController.requireLogin(), SubscriptionGroupController.redirectToSubscriptionGroupAdminPage - webRouter.delete '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.removeSelfFromGroup - - # Team invites - webRouter.get '/subscription/invites/:token/', AuthenticationController.requireLogin(), - TeamInvitesController.viewInvite - webRouter.put '/subscription/invites/:token/', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: 'team-invite', - maxRequests: 10 - timeInterval: 60 - }), - TeamInvitesController.acceptInvite - - #recurly callback - publicApiRouter.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback - - #user changes their account state - webRouter.post '/user/subscription/create', AuthenticationController.requireLogin(), SubscriptionController.createSubscription - webRouter.post '/user/subscription/update', AuthenticationController.requireLogin(), SubscriptionController.updateSubscription - webRouter.post '/user/subscription/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription - webRouter.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription - - webRouter.post '/user/subscription/v1/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelV1Subscription - - webRouter.put '/user/subscription/extend', AuthenticationController.requireLogin(), SubscriptionController.extendTrial - - webRouter.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage - webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan - - # Currently used in acceptance tests only, as a way to trigger the syncing logic - publicApiRouter.post "/user/:user_id/features/sync", AuthenticationController.httpAuth, SubscriptionController.refreshUserFeatures diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee deleted file mode 100644 index 1af9e17cc6..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ /dev/null @@ -1,108 +0,0 @@ -async = require("async") -_ = require("underscore") -Subscription = require('../../models/Subscription').Subscription -SubscriptionLocator = require("./SubscriptionLocator") -UserGetter = require("../User/UserGetter") -PlansLocator = require("./PlansLocator") -Settings = require("settings-sharelatex") -logger = require("logger-sharelatex") -ObjectId = require('mongoose').Types.ObjectId -FeaturesUpdater = require('./FeaturesUpdater') - -oneMonthInSeconds = 60 * 60 * 24 * 30 - -module.exports = SubscriptionUpdater = - syncSubscription: (recurlySubscription, adminUser_id, callback) -> - logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "syncSubscription, creating new if subscription does not exist" - SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> - return callback(err) if err? - if subscription? - logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does exist" - SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback - else - logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does not exist, creating a new one" - SubscriptionUpdater._createNewSubscription adminUser_id, (err, subscription)-> - return callback(err) if err? - SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback - - addUserToGroup: (subscriptionId, userId, callback)-> - @addUsersToGroup(subscriptionId, [userId], callback) - - addUsersToGroup: (subscriptionId, memberIds, callback)-> - @addUsersToGroupWithoutFeaturesRefresh subscriptionId, memberIds, (err) -> - return callback(err) if err? - - # Only apply features updates to users, not user stubs - UserGetter.getUsers memberIds, { _id: 1 }, (err, users) -> - return callback(err) if err? - - userIds = users.map (u) -> u._id.toString() - async.map userIds, FeaturesUpdater.refreshFeatures, callback - - addUsersToGroupWithoutFeaturesRefresh: (subscriptionId, memberIds, callback)-> - logger.log subscriptionId: subscriptionId, memberIds: memberIds, "adding members into mongo subscription" - searchOps = - _id: subscriptionId - insertOperation = - { $addToSet: { member_ids: { $each: memberIds } } } - - Subscription.findAndModify searchOps, insertOperation, callback - - removeUserFromGroups: (filter, user_id, callback)-> - removeOperation = - "$pull": {member_ids:user_id} - Subscription.updateMany filter, removeOperation, (err)-> - if err? - logger.err err:err, searchOps:searchOps, removeOperation:removeOperation, "error removing user from groups" - return callback(err) - UserGetter.getUserOrUserStubById user_id, {}, (error, user, isStub) -> - return callback(error) if error - return callback() if isStub - FeaturesUpdater.refreshFeatures user_id, callback - - removeUserFromGroup: (subscriptionId, user_id, callback)-> - SubscriptionUpdater.removeUserFromGroups { _id: subscriptionId }, user_id, callback - - removeUserFromAllGroups: (user_id, callback) -> - SubscriptionLocator.getMemberSubscriptions user_id, (error, subscriptions) -> - return callback(error) if error - return callback() unless subscriptions - subscriptionIds = subscriptions.map (sub) -> sub._id - SubscriptionUpdater.removeUserFromGroups { _id: subscriptionIds }, user_id, callback - - deleteWithV1Id: (v1TeamId, callback)-> - Subscription.deleteOne { "overleaf.id": v1TeamId }, callback - - deleteSubscription: (subscription_id, callback = (error) ->) -> - SubscriptionLocator.getSubscription subscription_id, (err, subscription) -> - return callback(err) if err? - affected_user_ids = [subscription.admin_id].concat(subscription.member_ids or []) - logger.log {subscription_id, affected_user_ids}, "deleting subscription and downgrading users" - Subscription.remove {_id: ObjectId(subscription_id)}, (err) -> - return callback(err) if err? - async.mapSeries affected_user_ids, FeaturesUpdater.refreshFeatures, callback - - _createNewSubscription: (adminUser_id, callback)-> - logger.log adminUser_id:adminUser_id, "creating new subscription" - subscription = new Subscription(admin_id:adminUser_id, manager_ids: [adminUser_id]) - subscription.save (err)-> - callback err, subscription - - _updateSubscriptionFromRecurly: (recurlySubscription, subscription, callback)-> - logger.log recurlySubscription:recurlySubscription, subscription:subscription, "updaing subscription" - if recurlySubscription.state == "expired" - return SubscriptionUpdater.deleteSubscription subscription._id, callback - subscription.recurlySubscription_id = recurlySubscription.uuid - subscription.planCode = recurlySubscription.plan.plan_code - plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) - if !plan? - return callback(new Error("plan code not found: #{subscription.planCode}")) - if plan.groupPlan - subscription.groupPlan = true - subscription.membersLimit = plan.membersLimit - subscription.save -> - allIds = _.union subscription.member_ids, [subscription.admin_id] - jobs = allIds.map (user_id)-> - return (cb)-> - FeaturesUpdater.refreshFeatures user_id, cb - async.series jobs, callback diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee deleted file mode 100644 index 2c55a1ee8e..0000000000 --- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee +++ /dev/null @@ -1,149 +0,0 @@ -Settings = require('settings-sharelatex') -RecurlyWrapper = require("./RecurlyWrapper") -PlansLocator = require("./PlansLocator") -SubscriptionFormatters = require("./SubscriptionFormatters") -LimitationsManager = require("./LimitationsManager") -SubscriptionLocator = require("./SubscriptionLocator") -V1SubscriptionManager = require("./V1SubscriptionManager") -InstitutionsGetter = require("../Institutions/InstitutionsGetter") -PublishersGetter = require("../Publishers/PublishersGetter") -sanitizeHtml = require 'sanitize-html' -logger = require('logger-sharelatex') -_ = require("underscore") -async = require('async') - - -buildBillingDetails = (recurlySubscription) -> - hostedLoginToken = recurlySubscription?.account?.hosted_login_token - recurlySubdomain = Settings?.apis?.recurly?.subdomain - if hostedLoginToken? && recurlySubdomain? - return [ - "https://", - recurlySubdomain, - ".recurly.com/account/billing_info/edit?ht=", - hostedLoginToken - ].join("") - -module.exports = - buildUsersSubscriptionViewModel: (user, callback = (error, data) ->) -> - async.auto { - personalSubscription: (cb) -> - SubscriptionLocator.getUsersSubscription user, cb - recurlySubscription: ['personalSubscription', (cb, {personalSubscription}) -> - if !personalSubscription?.recurlySubscription_id? or personalSubscription?.recurlySubscription_id == '' - return cb(null, null) - RecurlyWrapper.getSubscription personalSubscription.recurlySubscription_id, includeAccount: true, cb - ] - recurlyCoupons: ['recurlySubscription', (cb, {recurlySubscription}) -> - return cb(null, null) if !recurlySubscription - accountId = recurlySubscription.account.account_code - RecurlyWrapper.getAccountActiveCoupons accountId, cb - ] - plan: ['personalSubscription', (cb, {personalSubscription}) -> - return cb() if !personalSubscription? - plan = PlansLocator.findLocalPlanInSettings(personalSubscription.planCode) - return cb(new Error("No plan found for planCode '#{personalSubscription.planCode}'")) if !plan? - cb(null, plan) - ] - memberGroupSubscriptions: (cb) -> - SubscriptionLocator.getMemberSubscriptions user, cb - managedGroupSubscriptions: (cb) -> - SubscriptionLocator.getManagedGroupSubscriptions user, cb - confirmedMemberInstitutions: (cb) -> - InstitutionsGetter.getConfirmedInstitutions user._id, cb - managedInstitutions: (cb) -> - InstitutionsGetter.getManagedInstitutions user._id, cb - managedPublishers: (cb) -> - PublishersGetter.getManagedPublishers user._id, cb - v1SubscriptionStatus: (cb) -> - V1SubscriptionManager.getSubscriptionStatusFromV1 user._id, (error, status, v1Id) -> - return cb(error) if error? - cb(null, status) - }, (err, results) -> - return callback(err) if err? - { - personalSubscription, - memberGroupSubscriptions, - managedGroupSubscriptions, - confirmedMemberInstitutions, - managedInstitutions, - managedPublishers, - v1SubscriptionStatus, - recurlySubscription, - recurlyCoupons, - plan - } = results - memberGroupSubscriptions ?= [] - managedGroupSubscriptions ?= [] - confirmedMemberInstitutions ?= [] - managedInstitutions ?= [] - v1SubscriptionStatus ?= {} - recurlyCoupons ?= [] - - - if personalSubscription?.toObject? - # Downgrade from Mongoose object, so we can add a recurly and plan attribute - personalSubscription = personalSubscription.toObject() - - if plan? - personalSubscription.plan = plan - - if personalSubscription? and recurlySubscription? - tax = recurlySubscription?.tax_in_cents || 0 - personalSubscription.recurly = { - tax: tax - taxRate: parseFloat(recurlySubscription?.tax_rate?._) - billingDetailsLink: buildBillingDetails(recurlySubscription) - price: SubscriptionFormatters.formatPrice (recurlySubscription?.unit_amount_in_cents + tax), recurlySubscription?.currency - nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription?.current_period_ends_at) - currency: recurlySubscription.currency - state: recurlySubscription.state - trialEndsAtFormatted: SubscriptionFormatters.formatDate(recurlySubscription?.trial_ends_at) - trial_ends_at: recurlySubscription.trial_ends_at - activeCoupons: recurlyCoupons - } - - for memberGroupSubscription in memberGroupSubscriptions - if memberGroupSubscription.teamNotice - memberGroupSubscription.teamNotice = sanitizeHtml(memberGroupSubscription.teamNotice) - - callback null, { - personalSubscription, - managedGroupSubscriptions, - memberGroupSubscriptions, - confirmedMemberInstitutions, - managedInstitutions, - managedPublishers, - v1SubscriptionStatus - } - - buildViewModel : -> - plans = Settings.plans - - allPlans = {} - plans.forEach (plan)-> - allPlans[plan.planCode] = plan - - result = - allPlans: allPlans - - - result.personalAccount = _.find plans, (plan)-> - plan.planCode == "personal" - - result.studentAccounts = _.filter plans, (plan)-> - plan.planCode.indexOf("student") != -1 - - result.groupMonthlyPlans = _.filter plans, (plan)-> - plan.groupPlan and !plan.annual - - result.groupAnnualPlans = _.filter plans, (plan)-> - plan.groupPlan and plan.annual - - result.individualMonthlyPlans = _.filter plans, (plan)-> - !plan.groupPlan and !plan.annual and plan.planCode != "personal" and plan.planCode.indexOf("student") == -1 - - result.individualAnnualPlans = _.filter plans, (plan)-> - !plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1 - - return result diff --git a/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee b/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee deleted file mode 100644 index 72c28d3a79..0000000000 --- a/services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee +++ /dev/null @@ -1,70 +0,0 @@ -settings = require "settings-sharelatex" -logger = require("logger-sharelatex") -TeamInvitesHandler = require('./TeamInvitesHandler') -AuthenticationController = require("../Authentication/AuthenticationController") -SubscriptionLocator = require("./SubscriptionLocator") -ErrorController = require("../Errors/ErrorController") -EmailHelper = require("../Helpers/EmailHelper") - -module.exports = - createInvite: (req, res, next) -> - teamManagerId = AuthenticationController.getLoggedInUserId(req) - subscription = req.entity - email = EmailHelper.parseEmail(req.body.email) - if !email? - return res.status(422).json error: - code: 'invalid_email' - message: req.i18n.translate('invalid_email') - - - TeamInvitesHandler.createInvite teamManagerId, subscription, email, (err, invite) -> - return next(err) if err? - inviteView = { user: - { email: invite.email, sentAt: invite.sentAt, invite: true } - } - res.json inviteView - - viewInvite: (req, res, next) -> - token = req.params.token - userId = AuthenticationController.getLoggedInUserId(req) - - TeamInvitesHandler.getInvite token, (err, invite, teamSubscription) -> - return next(err) if err? - - if !invite - return ErrorController.notFound(req, res, next) - - SubscriptionLocator.getUsersSubscription userId, (err, personalSubscription) -> - return next(err) if err? - - hasIndividualRecurlySubscription = - personalSubscription? && - !personalSubscription.planCode.match(/(free|trial)/)? && - personalSubscription.groupPlan == false && - personalSubscription.recurlySubscription_id? && - personalSubscription.recurlySubscription_id != "" - - res.render "subscriptions/team/invite", - inviterName: invite.inviterName - inviteToken: invite.token - hasIndividualRecurlySubscription: hasIndividualRecurlySubscription - appName: settings.appName - - acceptInvite: (req, res, next) -> - token = req.params.token - userId = AuthenticationController.getLoggedInUserId(req) - - TeamInvitesHandler.acceptInvite token, userId, (err, results) -> - return next(err) if err? - res.sendStatus 204 - - revokeInvite: (req, res) -> - subscription = req.entity - email = EmailHelper.parseEmail(req.params.email) - teamManagerId = AuthenticationController.getLoggedInUserId(req) - if !email? - return res.sendStatus(400) - - TeamInvitesHandler.revokeInvite teamManagerId, subscription, email, (err, results) -> - return next(err) if err? - res.sendStatus 204 diff --git a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee deleted file mode 100644 index 43223ebd5c..0000000000 --- a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee +++ /dev/null @@ -1,158 +0,0 @@ -logger = require("logger-sharelatex") -crypto = require("crypto") -async = require("async") - -settings = require("settings-sharelatex") -ObjectId = require("mongojs").ObjectId - -TeamInvite = require("../../models/TeamInvite").TeamInvite -Subscription = require("../../models/Subscription").Subscription - -UserGetter = require("../User/UserGetter") -SubscriptionLocator = require("./SubscriptionLocator") -SubscriptionUpdater = require("./SubscriptionUpdater") -LimitationsManager = require("./LimitationsManager") - -EmailHandler = require("../Email/EmailHandler") -EmailHelper = require("../Helpers/EmailHelper") - -Errors = require "../Errors/Errors" - -module.exports = TeamInvitesHandler = - getInvite: (token, callback) -> - Subscription.findOne 'teamInvites.token': token, (err, subscription) -> - return callback(err) if err? - return callback(new Errors.NotFoundError('team not found')) unless subscription? - - invite = subscription.teamInvites.find (i) -> i.token == token - return callback(null, invite, subscription) - - createInvite: (teamManagerId, subscription, email, callback) -> - email = EmailHelper.parseEmail(email) - return callback(new Error('invalid email')) if !email? - logger.log {teamManagerId, email}, "Creating manager team invite" - UserGetter.getUser teamManagerId, (error, teamManager) -> - return callback(error) if error? - - if teamManager.first_name and teamManager.last_name - inviterName = "#{teamManager.first_name} #{teamManager.last_name} (#{teamManager.email})" - else - inviterName = teamManager.email - - removeLegacyInvite subscription.id, email, (error) -> - return callback(error) if error? - createInvite(subscription, email, inviterName, callback) - - importInvite: (subscription, inviterName, email, token, sentAt, callback) -> - checkIfInviteIsPossible subscription, email, (error, possible, reason) -> - return callback(error) if error? - return callback(reason) unless possible - - subscription.teamInvites.push({ - email: email - inviterName: inviterName - token: token - sentAt: sentAt - }) - - subscription.save callback - - acceptInvite: (token, userId, callback) -> - logger.log {userId}, "Accepting invite" - TeamInvitesHandler.getInvite token, (err, invite, subscription) -> - return callback(err) if err? - return callback(new Errors.NotFoundError('invite not found')) unless invite? - - SubscriptionUpdater.addUserToGroup subscription._id, userId, (err) -> - return callback(err) if err? - - removeInviteFromTeam(subscription.id, invite.email, callback) - - revokeInvite: (teamManagerId, subscription, email, callback) -> - email = EmailHelper.parseEmail(email) - return callback(new Error('invalid email')) if !email? - logger.log {teamManagerId, email}, "Revoking invite" - removeInviteFromTeam(subscription.id, email, callback) - - # Legacy method to allow a user to receive a confirmation email if their - # email is in Subscription.invited_emails when they join. We'll remove this - # after a short while. - createTeamInvitesForLegacyInvitedEmail: (email, callback) -> - SubscriptionLocator.getGroupsWithEmailInvite email, (err, teams) -> - return callback(err) if err? - - async.map teams, - (team, cb) -> TeamInvitesHandler.createInvite(team.admin_id, team, email, cb) - , callback - -createInvite = (subscription, email, inviterName, callback) -> - logger.log {subscriptionId: subscription.id, email, inviterName}, "Creating invite" - checkIfInviteIsPossible subscription, email, (error, possible, reason) -> - return callback(error) if error? - return callback(reason) unless possible - - invite = subscription.teamInvites.find (invite) -> invite.email == email - - if !invite? - invite = { - email: email - inviterName: inviterName - token: crypto.randomBytes(32).toString("hex") - sentAt: new Date() - } - subscription.teamInvites.push(invite) - else - invite.sentAt = new Date() - - subscription.save (error) -> - return callback(error) if error? - - opts = - to: email - inviterName: inviterName - acceptInviteUrl: "#{settings.siteUrl}/subscription/invites/#{invite.token}/" - appName: settings.appName - EmailHandler.sendEmail "verifyEmailToJoinTeam", opts, (error) -> - return callback(error, invite) - -removeInviteFromTeam = (subscriptionId, email, callback) -> - searchConditions = { _id: new ObjectId(subscriptionId.toString()) } - removeInvite = { $pull: { teamInvites: { email: email } } } - logger.log {subscriptionId, email, searchConditions, removeInvite}, 'removeInviteFromTeam' - - async.series [ - (cb) -> Subscription.update(searchConditions, removeInvite, cb), - (cb) -> removeLegacyInvite(subscriptionId, email, cb), - ], callback - -removeLegacyInvite = (subscriptionId, email, callback) -> - Subscription.update({ - _id: new ObjectId(subscriptionId.toString()) - }, { - $pull: { - invited_emails: email - } - }, callback) - -checkIfInviteIsPossible = (subscription, email, callback = (error, possible, reason) -> ) -> - unless subscription.groupPlan - logger.log {subscriptionId: subscription.id}, - "can not add members to a subscription that is not in a group plan" - return callback(null, false, wrongPlan: true) - - if LimitationsManager.teamHasReachedMemberLimit(subscription) - logger.log {subscriptionId: subscription.id}, "team has reached member limit" - return callback(null, false, limitReached: true) - - UserGetter.getUserByAnyEmail email, (error, existingUser) -> - return callback(error) if error? - return callback(null, true) unless existingUser? - - existingMember = subscription.member_ids.find (memberId) -> - memberId.toString() == existingUser._id.toString() - - if existingMember - logger.log {subscriptionId: subscription.id, email}, "user already in team" - return callback(null, false, alreadyInTeam: true) - else - return callback(null, true) diff --git a/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee deleted file mode 100644 index 3d20f40271..0000000000 --- a/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee +++ /dev/null @@ -1,12 +0,0 @@ -logger = require("logger-sharelatex") -User = require('../../models/User').User - -module.exports = - updateFeatures: (user_id, features, callback = (err, features, featuresChanged)->)-> - conditions = _id:user_id - update = {} - logger.log user_id:user_id, features:features, "updating users features" - update["features.#{key}"] = value for key, value of features - User.update conditions, update, (err, result)-> - callback err, features, result?.nModified == 1 - diff --git a/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee b/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee deleted file mode 100644 index 10c1a7983b..0000000000 --- a/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee +++ /dev/null @@ -1,103 +0,0 @@ -UserGetter = require "../User/UserGetter" -request = require "request" -settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -{ V1ConnectionError, NotFoundError } = require "../Errors/Errors" - -module.exports = V1SubscriptionManager = - # Returned planCode = 'v1_pro' | 'v1_pro_plus' | 'v1_student' | 'v1_free' | null - # For this to work, we need plans in settings with plan-codes: - # - 'v1_pro' - # - 'v1_pro_plus' - # - 'v1_student' - # - 'v1_free' - getPlanCodeFromV1: (userId, callback=(err, planCode, v1Id)->) -> - logger.log {userId}, "[V1SubscriptionManager] fetching v1 plan for user" - V1SubscriptionManager._v1Request userId, { - method: 'GET', - url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/plan_code" - }, (error, body, v1Id) -> - return callback(error) if error? - planName = body?.plan_name - logger.log {userId, planName, body}, "[V1SubscriptionManager] fetched v1 plan for user" - if planName in ['pro', 'pro_plus', 'student', 'free'] - planName = "v1_#{planName}" - else - # Throw away 'anonymous', etc as being equivalent to null - planName = null - return callback(null, planName, v1Id) - - notifyV1OfFeaturesChange: (userId, callback = (error) ->) -> - V1SubscriptionManager._v1Request userId, { - method: 'POST', - url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/sync" - }, callback - - getSubscriptionsFromV1: (userId, callback=(err, subscriptions, v1Id) ->) -> - V1SubscriptionManager._v1Request userId, { - method: 'GET', - url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/subscriptions" - }, callback - - getSubscriptionStatusFromV1: (userId, callback=(err, status) ->) -> - V1SubscriptionManager._v1Request userId, { - method: 'GET', - url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/subscription_status" - }, callback - - cancelV1Subscription: (userId, callback=(err)->) -> - V1SubscriptionManager._v1Request userId, { - method: 'DELETE', - url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/subscription" - }, callback - - v1IdForUser: (userId, callback=(err, v1Id) ->) -> - UserGetter.getUser userId, {'overleaf.id': 1}, (err, user) -> - return callback(err) if err? - v1Id = user?.overleaf?.id - if !v1Id? - logger.log {userId}, "[V1SubscriptionManager] no v1 id found for user" - - callback(null, v1Id) - - # v1 accounts created before migration to v2 had github and mendeley for free - # but these are now paid-for features for new accounts (v1id > cutoff) - getGrandfatheredFeaturesForV1User: (v1Id) -> - cutoff = settings.v1GrandfatheredFeaturesUidCutoff - return {} if !cutoff? - return {} if !v1Id? - - if (v1Id < cutoff) - return settings.v1GrandfatheredFeatures or {} - else - return {} - - _v1Request: (userId, options, callback=(err, body, v1Id)->) -> - if !settings?.apis?.v1 - return callback null, null - - V1SubscriptionManager.v1IdForUser userId, (err, v1Id) -> - return callback(err) if err? - return callback(null, null, null) if !v1Id? - request { - baseUrl: settings.apis.v1.url - url: options.url(v1Id) - method: options.method - auth: - user: settings.apis.v1.user - pass: settings.apis.v1.pass - sendImmediately: true - json: true, - timeout: 15 * 1000 - }, (error, response, body) -> - if error? - # Specially handle no connection err, so warning can be shown - error = new V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED' - return callback(error) - if 200 <= response.statusCode < 300 - return callback null, body, v1Id - else - if response.statusCode == 404 - return callback new NotFoundError("v1 user not found: #{userId}") - else - return callback new Error("non-success code from v1: #{response.statusCode} #{options.method} #{options.url(v1Id)}") diff --git a/services/web/app/coffee/Features/Subscription/planFeatures.coffee b/services/web/app/coffee/Features/Subscription/planFeatures.coffee deleted file mode 100644 index db5710341e..0000000000 --- a/services/web/app/coffee/Features/Subscription/planFeatures.coffee +++ /dev/null @@ -1,133 +0,0 @@ -module.exports = - [ - { - feature: 'number_collab' - value: 'str' - plans: { - free: '1' - coll: '10' - prof: 'unlimited' - } - student: '6' - } - { - feature: 'unlimited_private' - value: 'bool' - info: 'unlimited_private_info' - plans: { - free: true - coll: true - prof: true - }, - student: true - } - { - feature: 'realtime_collab' - value: 'bool' - info: 'realtime_collab_info' - plans: { - free: true - coll: true - prof: true - } - student: true - } - { - feature: 'thousands_templates' - value: 'bool' - info: 'hundreds_templates_info' - plans: { - free: true - coll: true - prof: true - } - student: true - } - { - feature: 'powerful_latex_editor' - value: 'bool' - info: 'latex_editor_info' - plans: { - free: true - coll: true - prof: true - } - student: true - } - { - feature: 'realtime_track_changes' - value: 'bool' - info: 'realtime_track_changes_info' - plans: { - free: false - coll: true - prof: true - }, - student: true - } - { - feature: 'reference_search' - value: 'bool' - info: 'reference_search_info' - plans: { - free: false - coll: true - prof: true - }, - student: true - }, - { - feature: 'reference_sync' - info: 'reference_sync_info' - value: 'bool' - plans: { - free: false - coll: true - prof: true - }, - student: true - } - { - feature: 'full_doc_history' - value: 'bool' - info: 'full_doc_history_info' - plans: { - free: false, - coll: true, - prof: true - }, - student: true - } - { - feature: 'dropbox_integration_lowercase' - value: 'bool' - info: 'dropbox_integration_info' - plans: { - free: false, - coll: true, - prof: true - }, - student: true - }, - { - feature: 'github_integration_lowercase' - value: 'bool' - info: 'github_integration_info' - plans: { - free: false, - coll: true, - prof: true - }, - student: true - }, - { - feature: 'priority_support', - value: 'bool', - plans: { - free: false, - coll: true, - prof: true - }, - student: true - }, - ] \ No newline at end of file diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee deleted file mode 100644 index b26b88e4f2..0000000000 --- a/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee +++ /dev/null @@ -1,63 +0,0 @@ -logger = require 'logger-sharelatex' -SudoModeHandler = require './SudoModeHandler' -AuthenticationController = require '../Authentication/AuthenticationController' -ObjectId = require('../../infrastructure/Mongoose').mongo.ObjectId -UserGetter = require '../User/UserGetter' -Settings = require 'settings-sharelatex' - - -module.exports = SudoModeController = - - sudoModePrompt: (req, res, next) -> - if req.externalAuthenticationSystemUsed() and !Settings.overleaf? - logger.log {userId}, "[SudoMode] using external auth, redirecting" - return res.redirect('/project') - userId = AuthenticationController.getLoggedInUserId(req) - logger.log {userId}, "[SudoMode] rendering sudo mode password page" - SudoModeHandler.isSudoModeActive userId, (err, isActive) -> - if err? - logger.err {err, userId}, "[SudoMode] error checking if sudo mode is active" - return next(err) - if isActive - logger.log {userId}, "[SudoMode] sudo mode already active, redirecting" - return res.redirect('/project') - res.render 'sudo_mode/sudo_mode_prompt', title: 'confirm_password_to_continue' - - submitPassword: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - redir = AuthenticationController._getRedirectFromSession(req) || "/project" - password = req.body.password - if !password - logger.log {userId}, "[SudoMode] no password supplied, failed authentication" - return next(new Error('no password supplied')) - logger.log {userId, redir}, "[SudoMode] checking user password" - UserGetter.getUser ObjectId(userId), {email: 1}, (err, userRecord) -> - if err? - logger.err {err, userId}, "[SudoMode] error getting user" - return next(err) - if !userRecord? - err = new Error('user not found') - logger.err {err, userId}, "[SudoMode] user not found" - return next(err) - SudoModeHandler.authenticate userRecord.email, password, (err, user) -> - if err? - logger.err {err, userId}, "[SudoMode] error authenticating user" - return next(err) - if user? - logger.log {userId}, "[SudoMode] authenticated user, activating sudo mode" - SudoModeHandler.activateSudoMode userId, (err) -> - if err? - logger.err {err, userId}, "[SudoMode] error activating sudo mode" - return next(err) - return res.json { - redir: redir - } - else - logger.log {userId}, "[SudoMode] authentication failed for user" - return res.json { - message: { - text: req.i18n.translate("invalid_password"), - type: 'error' - } - } - diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee deleted file mode 100644 index 18e7bfd74f..0000000000 --- a/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee +++ /dev/null @@ -1,46 +0,0 @@ -RedisWrapper = require('../../infrastructure/RedisWrapper') -rclient = RedisWrapper.client('sudomode') -logger = require('logger-sharelatex') -AuthenticationManager = require '../Authentication/AuthenticationManager' -Settings = require 'settings-sharelatex' -V1Handler = require '../V1/V1Handler' -UserGetter = require '../User/UserGetter' - - -TIMEOUT_IN_SECONDS = 60 * 60 - - -module.exports = SudoModeHandler = - - _buildKey: (userId) -> - "SudoMode:{#{userId}}" - - authenticate: (email, password, callback=(err, user)->) -> - if Settings.overleaf? - V1Handler.authWithV1 email, password, (err, isValid, v1Profile) -> - if !isValid - return callback(null, null) - UserGetter.getUser {'overleaf.id': v1Profile.id}, callback - else - AuthenticationManager.authenticate {email}, password, callback - - activateSudoMode: (userId, callback=(err)->) -> - if !userId? - return callback(new Error('[SudoMode] user must be supplied')) - duration = TIMEOUT_IN_SECONDS - logger.log {userId, duration}, "[SudoMode] activating sudo mode for user" - rclient.set SudoModeHandler._buildKey(userId), '1', 'EX', duration, callback - - clearSudoMode: (userId, callback=(err)->) -> - if !userId? - return callback(new Error('[SudoMode] user must be supplied')) - logger.log {userId}, "[SudoMode] clearing sudo mode for user" - rclient.del SudoModeHandler._buildKey(userId), callback - - isSudoModeActive: (userId, callback=(err, isActive)->) -> - if !userId? - return callback(new Error('[SudoMode] user must be supplied')) - rclient.get SudoModeHandler._buildKey(userId), (err, result) -> - if err? - return callback(err) - callback(null, result == '1') diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeMiddleware.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeMiddleware.coffee deleted file mode 100644 index 8ef75a52ed..0000000000 --- a/services/web/app/coffee/Features/SudoMode/SudoModeMiddleware.coffee +++ /dev/null @@ -1,25 +0,0 @@ -logger = require 'logger-sharelatex' -SudoModeHandler = require './SudoModeHandler' -AuthenticationController = require '../Authentication/AuthenticationController' -Settings = require 'settings-sharelatex' - - -module.exports = SudoModeMiddleware = - - protectPage: (req, res, next) -> - if req.externalAuthenticationSystemUsed() and !Settings.overleaf? - logger.log {userId}, "[SudoMode] using external auth, skipping sudo-mode check" - return next() - userId = AuthenticationController.getLoggedInUserId(req) - logger.log {userId}, "[SudoMode] protecting endpoint, checking if sudo mode is active" - SudoModeHandler.isSudoModeActive userId, (err, isActive) -> - if err? - logger.err {err, userId}, "[SudoMode] error checking if sudo mode is active" - return next(err) - if isActive - logger.log {userId}, "[SudoMode] sudo mode active, continuing" - return next() - else - logger.log {userId}, "[SudoMode] sudo mode not active, redirecting" - AuthenticationController.setRedirectInSession(req) - return res.redirect('/confirm-password') diff --git a/services/web/app/coffee/Features/SystemMessages/SystemMessageManager.coffee b/services/web/app/coffee/Features/SystemMessages/SystemMessageManager.coffee deleted file mode 100644 index e3705b3577..0000000000 --- a/services/web/app/coffee/Features/SystemMessages/SystemMessageManager.coffee +++ /dev/null @@ -1,29 +0,0 @@ -SystemMessage = require("../../models/SystemMessage").SystemMessage - -module.exports = SystemMessageManager = - getMessages: (callback = (error, messages) ->) -> - if @_cachedMessages? - return callback null, @_cachedMessages - else - @getMessagesFromDB (error, messages) => - return callback(error) if error? - @_cachedMessages = messages - return callback null, messages - - getMessagesFromDB: (callback = (error, messages) ->) -> - SystemMessage.find {}, callback - - clearMessages: (callback = (error) ->) -> - SystemMessage.remove {}, callback - - createMessage: (content, callback = (error) ->) -> - message = new SystemMessage { content: content } - message.save callback - - clearCache: () -> - delete @_cachedMessages - -CACHE_TIMEOUT = 20 * 1000 # 20 seconds -setInterval () -> - SystemMessageManager.clearCache() -, CACHE_TIMEOUT diff --git a/services/web/app/coffee/Features/Tags/TagsController.coffee b/services/web/app/coffee/Features/Tags/TagsController.coffee deleted file mode 100644 index 0cd15ab5e7..0000000000 --- a/services/web/app/coffee/Features/Tags/TagsController.coffee +++ /dev/null @@ -1,55 +0,0 @@ -TagsHandler = require("./TagsHandler") -logger = require("logger-sharelatex") -AuthenticationController = require('../Authentication/AuthenticationController') - -module.exports = - getAllTags: (req, res, next)-> - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log {user_id}, "getting tags" - TagsHandler.getAllTags user_id, (error, allTags)-> - return next(error) if error? - res.json(allTags) - - createTag: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - name = req.body.name - logger.log {user_id, name}, "creating tag" - TagsHandler.createTag user_id, name, (error, tag) -> - return next(error) if error? - res.json(tag) - - addProjectToTag: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - {tag_id, project_id} = req.params - logger.log {user_id, tag_id, project_id}, "adding tag to project" - TagsHandler.addProjectToTag user_id, tag_id, project_id, (error) -> - return next(error) if error? - res.status(204).end() - - removeProjectFromTag: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - {tag_id, project_id} = req.params - logger.log {user_id, tag_id, project_id}, "removing tag from project" - TagsHandler.removeProjectFromTag user_id, tag_id, project_id, (error) -> - return next(error) if error? - res.status(204).end() - - deleteTag: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - tag_id = req.params.tag_id - logger.log {user_id, tag_id}, "deleting tag" - TagsHandler.deleteTag user_id, tag_id, (error) -> - return next(error) if error? - res.status(204).end() - - renameTag: (req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - tag_id = req.params.tag_id - name = req.body?.name - if !name? - return res.status(400).end() - else - logger.log {user_id, tag_id, name}, "renaming tag" - TagsHandler.renameTag user_id, tag_id, name, (error) -> - return next(error) if error? - res.status(204).end() diff --git a/services/web/app/coffee/Features/Tags/TagsHandler.coffee b/services/web/app/coffee/Features/Tags/TagsHandler.coffee deleted file mode 100644 index 5a0c746295..0000000000 --- a/services/web/app/coffee/Features/Tags/TagsHandler.coffee +++ /dev/null @@ -1,112 +0,0 @@ -_ = require('underscore') -settings = require("settings-sharelatex") -request = require("request") -logger = require("logger-sharelatex") - -TIMEOUT = 1000 -module.exports = TagsHandler = - getAllTags: (user_id, callback)-> - @_requestTags user_id, (err, allTags)=> - if !allTags? - allTags = [] - @_groupTagsByProject allTags, (err, groupedByProject)-> - logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "got all tags from tags api" - callback err, allTags, groupedByProject - - createTag: (user_id, name, callback = (error, tag) ->) -> - opts = - url: "#{settings.apis.tags.url}/user/#{user_id}/tag" - json: - name: name - timeout: TIMEOUT - request.post opts, (err, res, body)-> - TagsHandler._handleResponse err, res, {user_id}, (error) -> - return callback(error) if error? - callback(null, body or {}) - - renameTag: (user_id, tag_id, name, callback = (error) ->) -> - url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}/rename" - request.post { - url: url - json: - name: name - timeout: TIMEOUT - }, (err, res, body) -> - TagsHandler._handleResponse err, res, {url, user_id, tag_id, name}, callback - - deleteTag: (user_id, tag_id, callback = (error) ->) -> - url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}" - request.del {url, timeout: TIMEOUT}, (err, res, body) -> - TagsHandler._handleResponse err, res, {url, user_id, tag_id}, callback - - updateTagUserIds: (old_user_id, new_user_id, callback) -> - opts = - url: "#{settings.apis.tags.url}/user/#{old_user_id}/tag" - json: - user_id: new_user_id - timeout: TIMEOUT - request.put opts, (err, res, body)-> - TagsHandler._handleResponse err, res, {old_user_id, new_user_id}, callback - - removeProjectFromTag: (user_id, tag_id, project_id, callback)-> - url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}/project/#{project_id}" - request.del {url, timeout: TIMEOUT}, (err, res, body) -> - TagsHandler._handleResponse err, res, {url, user_id, tag_id, project_id}, callback - - addProjectToTag: (user_id, tag_id, project_id, callback)-> - url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}/project/#{project_id}" - request.post {url, timeout: TIMEOUT}, (err, res, body) -> - TagsHandler._handleResponse err, res, {url, user_id, tag_id, project_id}, callback - - addProjectToTagName: (user_id, name, project_id, callback)-> - url = "#{settings.apis.tags.url}/user/#{user_id}/tag/project/#{project_id}" - opts = - json: { name } - timeout: TIMEOUT - url: url - request.post opts, (err, res, body) -> - TagsHandler._handleResponse err, res, {url, user_id, name, project_id}, callback - - removeProjectFromAllTags: (user_id, project_id, callback)-> - url = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}" - opts = - url: url - timeout:TIMEOUT - request.del opts, (err, res, body) -> - TagsHandler._handleResponse err, res, {url, user_id, project_id}, callback - - _handleResponse: (err, res, params, callback) -> - if err? - params.err = err - logger.err params, "error in tag api" - return callback(err) - else if res? and res.statusCode >= 200 and res.statusCode < 300 - return callback(null) - else - err = new Error("tags api returned a failure status code: #{res?.statusCode}") - params.err = err - logger.err params, "tags api returned failure status code: #{res?.statusCode}" - return callback(err) - - _requestTags: (user_id, callback)-> - opts = - url: "#{settings.apis.tags.url}/user/#{user_id}/tag" - json: true - timeout: TIMEOUT - request.get opts, (err, res, body)-> - TagsHandler._handleResponse err, res, {user_id}, (error) -> - return callback(error, []) if error? - callback(null, body or []) - - _groupTagsByProject: (tags, callback)-> - result = {} - _.each tags, (tag)-> - _.each tag.project_ids, (project_id)-> - result[project_id] = [] - - _.each tags, (tag)-> - _.each tag.project_ids, (project_id)-> - clonedTag = _.clone(tag) - delete clonedTag.project_ids - result[project_id].push(clonedTag) - callback null, result diff --git a/services/web/app/coffee/Features/Templates/TemplatesController.coffee b/services/web/app/coffee/Features/Templates/TemplatesController.coffee deleted file mode 100644 index 0f6c896bb9..0000000000 --- a/services/web/app/coffee/Features/Templates/TemplatesController.coffee +++ /dev/null @@ -1,30 +0,0 @@ -path = require('path') -AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController') -TemplatesManager = require('./TemplatesManager') -ProjectHelper = require('../../../js/Features/Project/ProjectHelper') -logger = require('logger-sharelatex') - -module.exports = TemplatesController = - - getV1Template: (req, res)-> - templateVersionId = req.params.Template_version_id - templateId = req.query.id - if !/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId) - logger.err templateVersionId:templateVersionId, templateId: templateId, "invalid template id or version" - return res.sendStatus 400 - data = {} - data.templateVersionId = templateVersionId - data.templateId = templateId - data.name = req.query.templateName - data.compiler = ProjectHelper.compilerFromV1Engine(req.query.latexEngine) - data.imageName = req.query.texImage - data.mainFile = req.query.mainFile - data.brandVariationId = req.query.brandVariationId - res.render path.resolve(__dirname, "../../../views/project/editor/new_from_template"), data - - createProjectFromV1Template: (req, res, next)-> - user_id = AuthenticationController.getLoggedInUserId(req) - TemplatesManager.createProjectFromV1Template req.body.brandVariationId, req.body.compiler, req.body.mainFile, req.body.templateId, req.body.templateName, req.body.templateVersionId, user_id, req.body.imageName, (err, project) -> - return next err if err? - delete req.session.templateData - res.redirect "/project/#{project._id}" diff --git a/services/web/app/coffee/Features/Templates/TemplatesManager.coffee b/services/web/app/coffee/Features/Templates/TemplatesManager.coffee deleted file mode 100644 index 8e95649672..0000000000 --- a/services/web/app/coffee/Features/Templates/TemplatesManager.coffee +++ /dev/null @@ -1,70 +0,0 @@ -Project = require('../../../js/models/Project').Project -ProjectDetailsHandler = require "../../../js/Features/Project/ProjectDetailsHandler" -ProjectOptionsHandler = require "../../../js/Features/Project/ProjectOptionsHandler" -ProjectRootDocManager = require "../../../js/Features/Project/ProjectRootDocManager" -ProjectUploadManager = require "../../../js/Features/Uploads/ProjectUploadManager" -FileWriter = require "../../infrastructure/FileWriter" -async = require "async" -fs = require "fs" -logger = require "logger-sharelatex" -request = require "request" -settings = require "settings-sharelatex" -uuid = require "uuid" - -module.exports = TemplatesManager = - createProjectFromV1Template: (brandVariationId, compiler, mainFile, templateId, templateName, templateVersionId, user_id, imageName, callback) -> - zipUrl = "#{settings.apis.v1.url}/api/v1/sharelatex/templates/#{templateVersionId}" - zipReq = request zipUrl, { - auth: - user: settings.apis.v1.user - pass: settings.apis.v1.pass - } - zipReq.on "error", (err) -> - logger.error { err }, "error getting zip from template API" - callback err - FileWriter.ensureDumpFolderExists (err) -> - return callback(err) if err? - - projectName = ProjectDetailsHandler.fixProjectName templateName - dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}" - writeStream = fs.createWriteStream dumpPath - writeStream.on "close", -> - if zipReq.response.statusCode != 200 - logger.err { uri: zipUrl, statusCode: zipReq.response.statusCode }, "non-success code getting zip from template API" - return callback new Error("get zip failed") - ProjectUploadManager.createProjectFromZipArchiveWithName user_id, projectName, dumpPath, (err, project) -> - if err? - logger.err { err, zipReq }, "problem building project from zip" - return callback err - async.series [ - (cb) -> TemplatesManager._setCompiler project._id, compiler, cb - (cb) -> TemplatesManager._setImage project._id, imageName, cb - (cb) -> TemplatesManager._setMainFile project._id, mainFile, cb - (cb) -> TemplatesManager._setBrandVariationId project._id, brandVariationId, cb - ], (err) -> - return callback err if err? - fs.unlink dumpPath, (err) -> - logger.err {err}, "error unlinking template zip" if err? - update = - fromV1TemplateId: templateId, - fromV1TemplateVersionId: templateVersionId - Project.update { _id: project._id }, update, {}, (err) -> - return callback err if err? - callback null, project - zipReq.pipe(writeStream) - - _setCompiler: (project_id, compiler, callback) -> - return callback() unless compiler? - ProjectOptionsHandler.setCompiler project_id, compiler, callback - - _setImage: (project_id, imageName, callback) -> - imageName ||= "wl_texlive:2018.1" - ProjectOptionsHandler.setImageName project_id, imageName, callback - - _setMainFile: (project_id, mainFile, callback) -> - return callback() unless mainFile? - ProjectRootDocManager.setRootDocFromName project_id, mainFile, callback - - _setBrandVariationId: (project_id, brandVariationId, callback) -> - return callback() unless brandVariationId? - ProjectOptionsHandler.setBrandVariationId project_id, brandVariationId, callback diff --git a/services/web/app/coffee/Features/Templates/TemplatesMiddleware.coffee b/services/web/app/coffee/Features/Templates/TemplatesMiddleware.coffee deleted file mode 100644 index 8baa0ca605..0000000000 --- a/services/web/app/coffee/Features/Templates/TemplatesMiddleware.coffee +++ /dev/null @@ -1,9 +0,0 @@ -settings = require("settings-sharelatex") -logger = require("logger-sharelatex") - - -module.exports = - saveTemplateDataInSession: (req, res, next)-> - if req.query.templateName - req.session.templateData = req.query - next() diff --git a/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee b/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee deleted file mode 100644 index 51ddf95b29..0000000000 --- a/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee +++ /dev/null @@ -1,15 +0,0 @@ -AuthenticationController = require('../Authentication/AuthenticationController') -TemplatesController = require("./TemplatesController") -TemplatesMiddleware = require('./TemplatesMiddleware') -RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') - -module.exports = - apply: (app)-> - - app.get '/project/new/template/:Template_version_id', TemplatesMiddleware.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.getV1Template - - app.post '/project/new/template', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit({ - endpointName: "create-project-from-template" - maxRequests: 20 - timeInterval: 60 - }), TemplatesController.createProjectFromV1Template diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee deleted file mode 100644 index 9dcce6e9c8..0000000000 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee +++ /dev/null @@ -1,75 +0,0 @@ -tpdsUpdateHandler = require('./TpdsUpdateHandler') -UpdateMerger = require "./UpdateMerger" -logger = require('logger-sharelatex') -Path = require('path') -metrics = require("metrics-sharelatex") - -module.exports = - # mergeUpdate and deleteUpdate are used by Dropbox, where the project is only passed as the name, as the - # first part of the file path. They have to check the project exists, find it, and create it if not. - # They also ignore 'noisy' files like .DS_Store, .gitignore, etc. - mergeUpdate: (req, res)-> - metrics.inc("tpds.merge-update") - {filePath, user_id, projectName} = parseParams(req) - source = req.headers["x-sl-update-source"] or "unknown" - logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], projectName:projectName, source: source, "reciving update request from tpds" - tpdsUpdateHandler.newUpdate user_id, projectName, filePath, req, source, (err)-> - logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], "sending response that tpdsUpdate has been completed" - if err? - logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds" - res.sendStatus(500) - else - logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds update has been processed" - res.sendStatus 200 - - - deleteUpdate: (req, res)-> - metrics.inc("tpds.delete-update") - {filePath, user_id, projectName} = parseParams(req) - source = req.headers["x-sl-update-source"] or "unknown" - logger.log user_id:user_id, filePath:filePath, projectName:projectName, fullPath:req.params[0], source: source, "reciving delete request from tpds" - tpdsUpdateHandler.deleteUpdate user_id, projectName, filePath, source, (err)-> - if err? - logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds" - res.sendStatus(500) - else - logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds delete has been processed" - res.sendStatus 200 - - # updateProjectContents and deleteProjectContents are used by GitHub. The project_id is known so we - # can skip right ahead to creating/updating/deleting the file. These methods will not ignore noisy - # files like .DS_Store, .gitignore, etc because people are generally more explicit with the files they - # want in git. - updateProjectContents: (req, res, next = (error) ->) -> - {project_id} = req.params - path = "/" + req.params[0] # UpdateMerger expects leading slash - source = req.headers["x-sl-update-source"] or "unknown" - logger.log project_id: project_id, path: path, source: source, "received project contents update" - UpdateMerger.mergeUpdate null, project_id, path, req, source, (error) -> - return next(error) if error? - res.sendStatus(200) - - deleteProjectContents: (req, res, next = (error) ->) -> - {project_id} = req.params - path = "/" + req.params[0] # UpdateMerger expects leading slash - source = req.headers["x-sl-update-source"] or "unknown" - logger.log project_id: project_id, path: path, source: source, "received project contents delete request" - UpdateMerger.deleteUpdate null, project_id, path, source, (error) -> - return next(error) if error? - res.sendStatus(200) - - parseParams: parseParams = (req)-> - path = req.params[0] - user_id = req.params.user_id - - path = Path.join("/",path) - if path.substring(1).indexOf('/') == -1 - filePath = "/" - projectName = path.substring(1) - else - filePath = path.substring(path.indexOf("/",1)) - projectName = path.substring(0, path.indexOf("/",1)) - projectName = projectName.replace("/","") - - return filePath:filePath, user_id:user_id, projectName:projectName - diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee deleted file mode 100644 index 78e3f12ed6..0000000000 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee +++ /dev/null @@ -1,54 +0,0 @@ -updateMerger = require('./UpdateMerger') -logger = require('logger-sharelatex') -projectLocator = require('../Project/ProjectLocator') -projectCreationHandler = require('../Project/ProjectCreationHandler') -projectDeleter = require('../Project/ProjectDeleter') -ProjectRootDocManager = require "../Project/ProjectRootDocManager" -FileTypeManager = require('../Uploads/FileTypeManager') -CooldownManager = require('../Cooldown/CooldownManager') -Errors = require('../Errors/Errors') - -commitMessage = "Before update from Dropbox" - -module.exports = - - newUpdate: (user_id, projectName, path, updateRequest, source, callback)-> - getOrCreateProject = (cb)=> - projectLocator.findUsersProjectByName user_id, projectName, (err, project)=> - logger.log user_id:user_id, filePath:path, projectName:projectName, "handling new update from tpds" - if !project? - projectCreationHandler.createBlankProject user_id, projectName, (err, project)=> - # have a crack at setting the root doc after a while, on creation we won't have it yet, but should have - # been sent it it within 30 seconds - setTimeout (-> ProjectRootDocManager.setRootDocAutomatically project._id ), @_rootDocTimeoutLength - cb err, project - else - cb err, project - getOrCreateProject (err, project)-> - return callback(err) if err? - CooldownManager.isProjectOnCooldown project._id, (err, projectIsOnCooldown) -> - return callback(err) if err? - if projectIsOnCooldown - logger.log {projectId: project._id}, "project is on cooldown, denying request" - return callback(new Errors.TooManyRequestsError('project on cooldown')) - FileTypeManager.shouldIgnore path, (err, shouldIgnore)-> - if shouldIgnore - return callback() - updateMerger.mergeUpdate user_id, project._id, path, updateRequest, source, callback - - - deleteUpdate: (user_id, projectName, path, source, callback)-> - logger.log user_id:user_id, filePath:path, "handling delete update from tpds" - projectLocator.findUsersProjectByName user_id, projectName, (err, project)-> - if !project? - logger.log user_id:user_id, filePath:path, projectName:projectName, "project not found from tpds update, ignoring folder or project" - return callback() - if path == "/" - logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project found for delete update, path is root so marking project as deleted" - return projectDeleter.markAsDeletedByExternalSource project._id, callback - else - updateMerger.deleteUpdate user_id, project._id, path, source, (err)-> - callback(err) - - - _rootDocTimeoutLength : 30 * 1000 diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee deleted file mode 100644 index 488f62bfa1..0000000000 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ /dev/null @@ -1,142 +0,0 @@ -settings = require('settings-sharelatex') -logger = require('logger-sharelatex') -path = require('path') -ProjectGetter = require('../Project/ProjectGetter') -keys = require('../../infrastructure/Keys') -metrics = require("metrics-sharelatex") -request = require("request") -CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') - -buildPath = (user_id, project_name, filePath)-> - projectPath = path.join(project_name, "/", filePath) - projectPath = encodeURIComponent(projectPath) - fullPath = path.join("/user/", "#{user_id}", "/entity/",projectPath) - return fullPath - - - - -tpdsworkerEnabled = -> settings.apis.tpdsworker?.url? -if !tpdsworkerEnabled() - logger.log "tpdsworker is not enabled, request will not be sent to it" - - -if settings.apis.thirdPartyDataStore.linode_url? - tpdsUrl = settings.apis.thirdPartyDataStore.linode_url -else - tpdsUrl = settings.apis.thirdPartyDataStore.url - -module.exports = TpdsUpdateSender = - - _enqueue: (group, method, job, callback)-> - if !tpdsworkerEnabled() - return callback() - opts = - uri:"#{settings.apis.tpdsworker.url}/enqueue/web_to_tpds_http_requests" - json : - group:group - method:method - job:job - method:"post" - timeout: (5 * 1000) - request opts, (err)-> - if err? - logger.err err:err, "error queuing something in the tpdsworker, continuing anyway" - callback() - else - logger.log group:group, job:job, "successfully queued up job for tpdsworker" - callback() - - _addEntity: (options, callback = (err)->)-> - getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> - if err? - logger.err err:err, options:options, "error getting projects user ids" - return callback(err) - logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store" - postOptions = - method : "post" - headers: - sl_entity_rev:options.rev - sl_project_id:options.project_id - sl_all_user_ids:JSON.stringify(allUserIds) - uri : "#{tpdsUrl}#{buildPath(user_id, options.project_name, options.path)}" - title: "addFile" - streamOrigin : options.streamOrigin - TpdsUpdateSender._enqueue options.project_id, "pipeStreamFrom", postOptions, (err)-> - if err? - logger.err err:err, project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "error sending file to third party data store queued up for processing" - return callback(err) - logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store queued up for processing" - callback(err) - - addFile : (options, callback = (err)->)-> - metrics.inc("tpds.add-file") - options.streamOrigin = (settings.apis.filestore.linode_url or settings.apis.filestore.url) + path.join("/project/#{options.project_id}/file/","#{options.file_id}") - @_addEntity(options, callback) - - addDoc : (options, callback = (err)->)-> - metrics.inc("tpds.add-doc") - options.streamOrigin = (settings.apis.docstore.linode_url or settings.apis.docstore.pubUrl) + path.join("/project/#{options.project_id}/doc/","#{options.doc_id}/raw") - @_addEntity(options, callback) -   - - moveEntity : (options, callback = (err)->)-> - metrics.inc("tpds.move-entity") - if options.newProjectName? - startPath = path.join("/#{options.project_name}/") - endPath = path.join("/#{options.newProjectName}/") - else - startPath = mergeProjectNameAndPath(options.project_name, options.startPath) - endPath = mergeProjectNameAndPath(options.project_name, options.endPath) - getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> - logger.log project_id: options.project_id, user_id:user_id, startPath:startPath, endPath:endPath, uri:options.uri, "moving entity in third party data store" - moveOptions = - method : "put" - title:"moveEntity" - uri : "#{tpdsUrl}/user/#{user_id}/entity" - headers: - sl_project_id:options.project_id, - sl_entity_rev:options.rev - sl_all_user_ids:JSON.stringify(allUserIds) - json : - user_id : user_id - endPath: endPath - startPath: startPath - TpdsUpdateSender._enqueue options.project_id, "standardHttpRequest", moveOptions, callback - - deleteEntity : (options, callback = (err)->)-> - metrics.inc("tpds.delete-entity") - getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> - logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, "deleting entity in third party data store" - deleteOptions = - method : "DELETE" - headers: - sl_project_id:options.project_id - sl_all_user_ids:JSON.stringify(allUserIds) - uri : "#{tpdsUrl}#{buildPath(user_id, options.project_name, options.path)}" - title:"deleteEntity" - sl_all_user_ids:JSON.stringify(allUserIds) - TpdsUpdateSender._enqueue options.project_id, "standardHttpRequest", deleteOptions, callback - - pollDropboxForUser: (user_id, callback = (err) ->) -> - metrics.inc("tpds.poll-dropbox") - logger.log user_id: user_id, "polling dropbox for user" - options = - method: "POST" - uri:"#{tpdsUrl}/user/poll" - json: - user_ids: [user_id] - TpdsUpdateSender._enqueue "poll-dropbox:#{user_id}", "standardHttpRequest", options, callback - -getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)-> - ProjectGetter.getProject project_id, {_id: true, owner_ref: true}, (err, project) -> - return callback(err) if err? - CollaboratorsHandler.getInvitedMemberIds project_id, (err, member_ids) -> - return callback(err) if err? - callback err, project?.owner_ref, member_ids - -mergeProjectNameAndPath = (project_name, path)-> - if(path.indexOf('/') == 0) - path = path.substring(1) - fullPath = "/#{project_name}/#{path}" - return fullPath diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee deleted file mode 100644 index 8356a7cb91..0000000000 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee +++ /dev/null @@ -1,74 +0,0 @@ -_ = require('underscore') -fs = require('fs') -logger = require('logger-sharelatex') -EditorController = require('../Editor/EditorController') -FileTypeManager = require('../Uploads/FileTypeManager') -FileWriter = require('../../infrastructure/FileWriter') -ProjectEntityHandler = require('../Project/ProjectEntityHandler') - -module.exports = UpdateMerger = - mergeUpdate: (user_id, project_id, path, updateRequest, source, callback = (error) ->)-> - logger.log project_id:project_id, path:path, "merging update from tpds" - FileWriter.writeStreamToDisk project_id, updateRequest, (err, fsPath)-> - return callback(err) if err? - UpdateMerger._mergeUpdate user_id, project_id, path, fsPath, source, (mergeErr) -> - fs.unlink fsPath, (deleteErr) -> - if deleteErr? - logger.err project_id:project_id, fsPath:fsPath, "error deleting file" - callback mergeErr - - _determineFileType: (project_id, path, fsPath, callback = (err, fileType) ->) -> - ProjectEntityHandler.getAllEntities project_id, (err, docs, files) -> - return callback(err) if err? - if _.some(files, (f) -> f.path is path) - return callback(null, "existing-file") - if _.some(docs, (d) -> d.path is path) - return callback(null, "existing-doc") - # existing file not found in project, so check the file type to determine if doc - FileTypeManager.getType path, fsPath, (err, isBinary)-> - return callback(err) if err? - if isBinary - callback(null, "new-file") # extension was not text - else - callback(null, "new-doc") - - _mergeUpdate: (user_id, project_id, path, fsPath, source, callback = (error) ->)-> - UpdateMerger._determineFileType project_id, path, fsPath, (err, fileType)-> - return callback(err) if err? - if fileType in ["existing-file", "new-file"] - UpdateMerger.p.processFile project_id, fsPath, path, source, user_id, callback - else if fileType in ["existing-doc", "new-doc"] - UpdateMerger.p.processDoc project_id, user_id, fsPath, path, source, callback - else - callback new Error("unrecognized file") - - deleteUpdate: (user_id, project_id, path, source, callback = () ->)-> - EditorController.deleteEntityWithPath project_id, path, source, user_id, () -> - logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds" - callback() - - p: - - processDoc: (project_id, user_id, fsPath, path, source, callback)-> - UpdateMerger.p.readFileIntoTextArray fsPath, (err, docLines)-> - if err? - logger.err project_id:project_id, "error reading file into text array for process doc update" - return callback(err) - logger.log docLines:docLines, "processing doc update from tpds" - EditorController.upsertDocWithPath project_id, path, docLines, source, user_id, (err) -> - logger.log project_id:project_id, "completed processing file update from tpds" - callback(err) - - processFile: (project_id, fsPath, path, source, user_id, callback)-> - logger.log project_id:project_id, "processing file update from tpds" - EditorController.upsertFileWithPath project_id, path, fsPath, null, source, user_id, (err) -> - logger.log project_id:project_id, "completed processing file update from tpds" - callback(err) - - readFileIntoTextArray: (path, callback)-> - fs.readFile path, "utf8", (error, content = "") -> - if error? - logger.err path:path, "error reading file into text array" - return callback(error) - lines = content.split(/\r\n|\n|\r/) - callback error, lines diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee deleted file mode 100644 index bd90c65fb6..0000000000 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee +++ /dev/null @@ -1,154 +0,0 @@ -ProjectController = require "../Project/ProjectController" -AuthenticationController = require '../Authentication/AuthenticationController' -TokenAccessHandler = require './TokenAccessHandler' -Features = require '../../infrastructure/Features' -Errors = require '../Errors/Errors' -logger = require 'logger-sharelatex' -settings = require 'settings-sharelatex' - -module.exports = TokenAccessController = - - _loadEditor: (projectId, req, res, next) -> - req.params.Project_id = projectId.toString() - return ProjectController.loadEditor(req, res, next) - - _tryHigherAccess: (token, userId, req, res, next) -> - TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project) -> - if err? - logger.err {err, token, userId}, - "[TokenAccess] error finding project with higher access" - return next(err) - if !project? - logger.log {token, userId}, - "[TokenAccess] no project with higher access found for this user and token" - return next(new Errors.NotFoundError()) - logger.log {token, userId, projectId: project._id}, - "[TokenAccess] user has higher access to project, redirecting" - res.redirect(302, "/project/#{project._id}") - - readAndWriteToken: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - token = req.params['read_and_write_token'] - logger.log {userId, token}, "[TokenAccess] requesting read-and-write token access" - TokenAccessHandler.findProjectWithReadAndWriteToken token, (err, project, projectExists) -> - if err? - logger.err {err, token, userId}, - "[TokenAccess] error getting project by readAndWrite token" - return next(err) - if !projectExists and settings.overleaf - logger.log {token, userId}, - "[TokenAccess] no project found for this token" - TokenAccessController._handleV1Project( - token, - userId, - "/#{token}", - res, - next - ) - else if !project? - logger.log {token, userId}, - "[TokenAccess] no token-based project found for readAndWrite token" - if !userId? - logger.log {token}, - "[TokenAccess] No project found with read-write token, anonymous user, deny" - return next(new Errors.NotFoundError()) - TokenAccessController._tryHigherAccess(token, userId, req, res, next) - else - if !userId? - if TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED - logger.log {token, projectId: project._id}, - "[TokenAccess] allow anonymous read-and-write token access" - TokenAccessHandler.grantSessionTokenAccess(req, project._id, token) - req._anonymousAccessToken = token - return TokenAccessController._loadEditor(project._id, req, res, next) - else - logger.log {token, projectId: project._id}, - "[TokenAccess] deny anonymous read-and-write token access" - AuthenticationController.setRedirectInSession(req) - return res.redirect('/restricted') - if project.owner_ref.toString() == userId - logger.log {userId, projectId: project._id}, - "[TokenAccess] user is already project owner" - return TokenAccessController._loadEditor(project._id, req, res, next) - logger.log {userId, projectId: project._id}, - "[TokenAccess] adding user to project with readAndWrite token" - TokenAccessHandler.addReadAndWriteUserToProject userId, project._id, (err) -> - if err? - logger.err {err, token, userId, projectId: project._id}, - "[TokenAccess] error adding user to project with readAndWrite token" - return next(err) - return TokenAccessController._loadEditor(project._id, req, res, next) - - readOnlyToken: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - token = req.params['read_only_token'] - logger.log {userId, token}, "[TokenAccess] requesting read-only token access" - TokenAccessHandler.getV1DocPublishedInfo token, (err, doc_published_info) -> - return next err if err? - return res.redirect doc_published_info.published_path if doc_published_info.allow == false - - TokenAccessHandler.findProjectWithReadOnlyToken token, (err, project, projectExists) -> - if err? - logger.err {err, token, userId}, - "[TokenAccess] error getting project by readOnly token" - return next(err) - if !projectExists and settings.overleaf - logger.log {token, userId}, - "[TokenAccess] no project found for this token" - TokenAccessController._handleV1Project( - token, - userId, - "/read/#{token}", - res, - next - ) - else if !project? - logger.log {token, userId}, - "[TokenAccess] no project found for readOnly token" - if !userId? - logger.log {token}, - "[TokenAccess] No project found with readOnly token, anonymous user, deny" - return next(new Errors.NotFoundError()) - TokenAccessController._tryHigherAccess(token, userId, req, res, next) - else - if !userId? - logger.log {userId, projectId: project._id}, - "[TokenAccess] adding anonymous user to project with readOnly token" - TokenAccessHandler.grantSessionTokenAccess(req, project._id, token) - req._anonymousAccessToken = token - return TokenAccessController._loadEditor(project._id, req, res, next) - else - if project.owner_ref.toString() == userId - logger.log {userId, projectId: project._id}, - "[TokenAccess] user is already project owner" - return TokenAccessController._loadEditor(project._id, req, res, next) - logger.log {userId, projectId: project._id}, - "[TokenAccess] adding user to project with readOnly token" - TokenAccessHandler.addReadOnlyUserToProject userId, project._id, (err) -> - if err? - logger.err {err, token, userId, projectId: project._id}, - "[TokenAccess] error adding user to project with readAndWrite token" - return next(err) - return TokenAccessController._loadEditor(project._id, req, res, next) - - _handleV1Project: (token, userId, redirectPath, res, next) -> - if !userId? - if Features.hasFeature('force-import-to-v2') - return res.render('project/v2-import', { loginRedirect: redirectPath }) - else - return res.redirect(302, "/sign_in_to_v1?return_to=#{redirectPath}") - else - TokenAccessHandler.getV1DocInfo token, userId, (err, doc_info) -> - return next err if err? - return next(new Errors.NotFoundError()) if !doc_info.exists - return next(new Errors.NotFoundError()) if doc_info.exported - if Features.hasFeature('force-import-to-v2') - return res.render('project/v2-import', { - projectId: token, - hasOwner: doc_info.has_owner, - name: doc_info.name || 'Untitled', - hasAssignment: doc_info.has_assignment, - brandInfo: doc_info.brand_info - }) - else - return res.redirect(302, "/sign_in_to_v1?return_to=#{redirectPath}") diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee deleted file mode 100644 index 3c64bbff07..0000000000 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee +++ /dev/null @@ -1,174 +0,0 @@ -Project = require('../../models/Project').Project -CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') -PublicAccessLevels = require '../Authorization/PublicAccessLevels' -PrivilegeLevels = require '../Authorization/PrivilegeLevels' -UserGetter = require '../User/UserGetter' -ObjectId = require("mongojs").ObjectId -Settings = require('settings-sharelatex') -logger = require('logger-sharelatex') -V1Api = require "../V1/V1Api" -crypto = require 'crypto' - -module.exports = TokenAccessHandler = - - ANONYMOUS_READ_AND_WRITE_ENABLED: - Settings.allowAnonymousReadAndWriteSharing == true - - _extractNumericPrefix: (token) -> - token.match(/^(\d+)\w+/) - - _getProjectByReadOnlyToken: (token, callback=(err, project)->) -> - Project.findOne { - 'tokens.readOnly': token - }, {_id: 1, tokens: 1, publicAccesLevel: 1, owner_ref: 1}, callback - - _getProjectByEitherToken: (token, callback=(err, project)->) -> - TokenAccessHandler._getProjectByReadOnlyToken token, (err, project) -> - return callback(err) if err? - if project? - return callback(null, project) - TokenAccessHandler._getProjectByReadAndWriteToken token, (err, project) -> - return callback(err) if err? - callback(null, project) - - _getProjectByReadAndWriteToken: (token, callback=(err, project)->) -> - numericPrefixMatch = TokenAccessHandler._extractNumericPrefix(token) - if !numericPrefixMatch - return callback(null, null) - numerics = numericPrefixMatch[1] - Project.findOne { - 'tokens.readAndWritePrefix': numerics - }, {_id: 1, tokens: 1, publicAccesLevel: 1, owner_ref: 1}, (err, project) -> - return callback(err) if err? - if !project? - return callback(null, null) - try - if !crypto.timingSafeEqual(new Buffer(token), new Buffer(project.tokens.readAndWrite)) - logger.err {token}, "read-and-write token match on numeric section, but not on full token" - return callback(null, null) - else - return callback(null, project) - catch err - logger.err {token, cryptoErr: err}, "error comparing tokens" - return callback(null, null) - - findProjectWithReadOnlyToken: (token, callback=(err, project, projectExists)->) -> - TokenAccessHandler._getProjectByReadOnlyToken token, (err, project) -> - if err? - return callback(err) - if !project? - return callback(null, null, false) # Project doesn't exist, so we handle differently - if project.publicAccesLevel != PublicAccessLevels.TOKEN_BASED - return callback(null, null, true) # Project does exist, but it isn't token based - return callback(null, project, true) - - findProjectWithReadAndWriteToken: (token, callback=(err, project, projectExists)->) -> - TokenAccessHandler._getProjectByReadAndWriteToken token, (err, project) -> - if err? - return callback(err) - if !project? - return callback(null, null, false) # Project doesn't exist, so we handle differently - if project.publicAccesLevel != PublicAccessLevels.TOKEN_BASED - return callback(null, null, true) # Project does exist, but it isn't token based - return callback(null, project, true) - - _userIsMember: (userId, projectId, callback=(err, isMember)->) -> - CollaboratorsHandler.isUserInvitedMemberOfProject userId, projectId, callback - - findProjectWithHigherAccess: (token, userId, callback=(err, project)->) -> - TokenAccessHandler._getProjectByEitherToken token, (err, project) -> - return callback(err) if err? - if !project? - return callback(null, null) - projectId = project._id - TokenAccessHandler._userIsMember userId, projectId, (err, isMember) -> - return callback(err) if err? - callback( - null, - if isMember == true then project else null - ) - - addReadOnlyUserToProject: (userId, projectId, callback=(err)->) -> - userId = ObjectId(userId.toString()) - projectId = ObjectId(projectId.toString()) - Project.update { - _id: projectId - }, { - $addToSet: {tokenAccessReadOnly_refs: userId} - }, callback - - addReadAndWriteUserToProject: (userId, projectId, callback=(err)->) -> - userId = ObjectId(userId.toString()) - projectId = ObjectId(projectId.toString()) - Project.update { - _id: projectId - }, { - $addToSet: {tokenAccessReadAndWrite_refs: userId} - }, callback - - grantSessionTokenAccess: (req, projectId, token) -> - if req.session? - if !req.session.anonTokenAccess? - req.session.anonTokenAccess = {} - req.session.anonTokenAccess[projectId.toString()] = token.toString() - - getRequestToken: (req, projectId) -> - token = ( - req?.session?.anonTokenAccess?[projectId.toString()] or - req?.headers['x-sl-anonymous-access-token'] - ) - return token - - isValidToken: (projectId, token, callback=(err, isValidReadAndWrite, isValidReadOnly)->) -> - if !token - return callback null, false, false - _validate = (project) -> - project? and - project.publicAccesLevel == PublicAccessLevels.TOKEN_BASED and - project._id.toString() == projectId.toString() - TokenAccessHandler.findProjectWithReadAndWriteToken token, (err, readAndWriteProject) -> - return callback(err) if err? - isValidReadAndWrite = _validate(readAndWriteProject) - TokenAccessHandler.findProjectWithReadOnlyToken token, (err, readOnlyProject) -> - return callback(err) if err? - isValidReadOnly = _validate(readOnlyProject) - callback null, isValidReadAndWrite, isValidReadOnly - - protectTokens: (project, privilegeLevel) -> - if project? && project.tokens? - if privilegeLevel == PrivilegeLevels.OWNER - return - if privilegeLevel != PrivilegeLevels.READ_AND_WRITE - project.tokens.readAndWrite = '' - project.tokens.readAndWritePrefix = '' - if privilegeLevel != PrivilegeLevels.READ_ONLY - project.tokens.readOnly = '' - - getV1DocPublishedInfo: (token, callback = (err, publishedInfo) ->) -> - # default to allowing access - return callback(null, { - allow: true - }) unless Settings.apis?.v1? - - V1Api.request { url: "/api/v1/sharelatex/docs/#{token}/is_published" }, (err, response, body) -> - return callback err if err? - callback null, body - - getV1DocInfo: (token, v2UserId, callback=(err, info)->) -> - # default to not exported - return callback(null, { - exists: true - exported: false - }) unless Settings.apis?.v1? - - UserGetter.getUser v2UserId, { overleaf: 1 }, (err, user) -> - return callback(err) if err? - v1UserId = user.overleaf?.id - V1Api.request { url: "/api/v1/sharelatex/users/#{v1UserId}/docs/#{token}/info" }, (err, response, body) -> - return callback err if err? - callback null, body - -module.exports.READ_AND_WRITE_TOKEN_REGEX = /^(\d+)(\w+)$/ -module.exports.READ_AND_WRITE_URL_REGEX = /^\/(\d+)(\w+)$/ -module.exports.READ_ONLY_TOKEN_REGEX = /^([a-z]{12})$/ -module.exports.READ_ONLY_URL_REGEX = /^\/read\/([a-z]{12})$/ diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee deleted file mode 100644 index fe41454206..0000000000 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ /dev/null @@ -1,143 +0,0 @@ -logger = require "logger-sharelatex" -metrics = require "metrics-sharelatex" -fs = require "fs" -Path = require "path" -fse = require "fs-extra" -yauzl = require "yauzl" -Settings = require "settings-sharelatex" -Errors = require "../Errors/Errors" -_ = require("underscore") - -ONE_MEG = 1024 * 1024 - -module.exports = ArchiveManager = - - _isZipTooLarge: (source, callback = (err, isTooLarge)->)-> - callback = _.once callback - - totalSizeInBytes = null - yauzl.open source, {lazyEntries: true}, (err, zipfile) -> - return callback(new Errors.InvalidError("invalid_zip_file")) if err? - - if Settings.maxEntitiesPerProject? and zipfile.entryCount > Settings.maxEntitiesPerProject - return callback(null, true) # too many files in zip file - - zipfile.on "error", callback - - # read all the entries - zipfile.readEntry() - zipfile.on "entry", (entry) -> - totalSizeInBytes += entry.uncompressedSize - zipfile.readEntry() # get the next entry - - # no more entries to read - zipfile.on "end", () -> - if !totalSizeInBytes? or isNaN(totalSizeInBytes) - logger.err source:source, totalSizeInBytes:totalSizeInBytes, "error getting bytes of zip" - return callback(new Error("error getting bytes of zip")) - isTooLarge = totalSizeInBytes > (ONE_MEG * 300) - callback(null, isTooLarge) - - _checkFilePath: (entry, destination, callback = (err, destFile) ->) -> - # transform backslashes to forwardslashes to accommodate badly-behaved zip archives - transformedFilename = entry.fileName.replace(/\\/g, '/') - # check if the entry is a directory - endsWithSlash = /\/$/ - if endsWithSlash.test(transformedFilename) - return callback() # don't give a destfile for directory - # check that the file does not use a relative path - for dir in transformedFilename.split('/') - if dir == '..' - return callback(new Error("relative path")) - # check that the destination file path is normalized - dest = "#{destination}/#{transformedFilename}" - if dest != Path.normalize(dest) - return callback(new Error("unnormalized path")) - else - return callback(null, dest) - - _writeFileEntry: (zipfile, entry, destFile, callback = (err)->) -> - callback = _.once callback - - zipfile.openReadStream entry, (err, readStream) -> - return callback(err) if err? - readStream.on "error", callback - readStream.on "end", callback - - errorHandler = (err) -> # clean up before calling callback - readStream.unpipe() - readStream.destroy() - callback(err) - - fse.ensureDir Path.dirname(destFile), (err) -> - return errorHandler(err) if err? - writeStream = fs.createWriteStream destFile - writeStream.on 'error', errorHandler - readStream.pipe(writeStream) - - _extractZipFiles: (source, destination, callback = (err) ->) -> - callback = _.once callback - - yauzl.open source, {lazyEntries: true}, (err, zipfile) -> - return callback(err) if err? - zipfile.on "error", callback - # read all the entries - zipfile.readEntry() - zipfile.on "entry", (entry) -> - logger.log {source:source, fileName: entry.fileName}, "processing zip file entry" - ArchiveManager._checkFilePath entry, destination, (err, destFile) -> - if err? - logger.warn err:err, source:source, destination:destination, "skipping bad file path" - zipfile.readEntry() # bad path, just skip to the next file - return - if destFile? # only write files - ArchiveManager._writeFileEntry zipfile, entry, destFile, (err) -> - if err? - logger.error err:err, source:source, destFile:destFile, "error unzipping file entry" - zipfile.close() # bail out, stop reading file entries - return callback(err) - else - zipfile.readEntry() # continue to the next file - else # if it's a directory, continue - zipfile.readEntry() - # no more entries to read - zipfile.on "end", callback - - extractZipArchive: (source, destination, _callback = (err) ->) -> - callback = (args...) -> - _callback(args...) - _callback = () -> - - ArchiveManager._isZipTooLarge source, (err, isTooLarge)-> - if err? - logger.err err:err, "error checking size of zip file" - return callback(err) - - if isTooLarge - return callback(new Errors.InvalidError("zip_contents_too_large")) - - timer = new metrics.Timer("unzipDirectory") - logger.log source: source, destination: destination, "unzipping file" - - ArchiveManager._extractZipFiles source, destination, (err) -> - timer.done() - if err? - logger.error {err, source, destination}, "unzip failed" - callback(err) - else - callback() - - findTopLevelDirectory: (directory, callback = (error, topLevelDir) ->) -> - fs.readdir directory, (error, files) -> - return callback(error) if error? - if files.length == 1 - childPath = Path.join(directory, files[0]) - fs.stat childPath, (error, stat) -> - return callback(error) if error? - if stat.isDirectory() - return callback(null, childPath) - else - return callback(null, directory) - else - return callback(null, directory) - diff --git a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee deleted file mode 100644 index 92352bf744..0000000000 --- a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee +++ /dev/null @@ -1,94 +0,0 @@ -async = require "async" -fs = require "fs" -_ = require "underscore" -FileTypeManager = require "./FileTypeManager" -EditorController = require "../Editor/EditorController" -logger = require("logger-sharelatex") - -module.exports = FileSystemImportManager = - addDoc: (user_id, project_id, folder_id, name, path, charset, replace, callback = (error, doc)-> )-> - FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> - if !isSafe - logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, name:name, path:path, "add doc is from symlink, stopping process" - return callback("path is symlink") - fs.readFile path, charset, (error, content) -> - return callback(error) if error? - content = content.replace(/\r\n?/g, "\n") # convert Windows line endings to unix. very old macs also created \r-separated lines - lines = content.split("\n") - if replace - EditorController.upsertDoc project_id, folder_id, name, lines, "upload", user_id, callback - else - EditorController.addDoc project_id, folder_id, name, lines, "upload", user_id, callback - - addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )-> - FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> - if !isSafe - logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, name:name, path:path, "add file is from symlink, stopping insert" - return callback("path is symlink") - - if replace - EditorController.upsertFile project_id, folder_id, name, path, null, "upload", user_id, callback - else - EditorController.addFile project_id, folder_id, name, path, null, "upload", user_id, callback - - addFolder: (user_id, project_id, folder_id, name, path, replace, callback = (error)-> ) -> - FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> - if !isSafe - logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, path:path, "add folder is from symlink, stopping insert" - return callback("path is symlink") - EditorController.addFolder project_id, folder_id, name, "upload", (error, new_folder) => - return callback(error) if error? - FileSystemImportManager.addFolderContents user_id, project_id, new_folder._id, path, replace, (error) -> - return callback(error) if error? - callback null, new_folder - - addFolderContents: (user_id, project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) -> - FileSystemImportManager._isSafeOnFileSystem folderPath, (err, isSafe)-> - if !isSafe - logger.log user_id:user_id, project_id:project_id, parent_folder_id:parent_folder_id, folderPath:folderPath, "add folder contents is from symlink, stopping insert" - return callback("path is symlink") - fs.readdir folderPath, (error, entries = []) => - return callback(error) if error? - async.eachSeries( - entries, - (entry, callback) => - FileTypeManager.shouldIgnore entry, (error, ignore) => - return callback(error) if error? - if !ignore - FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback - else - callback() - callback - ) - - addEntity: (user_id, project_id, folder_id, name, path, replace, callback = (error, entity)-> ) -> - FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> - if !isSafe - logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, path:path, "add entry is from symlink, stopping insert" - return callback("path is symlink") - - FileTypeManager.isDirectory path, (error, isDirectory) => - return callback(error) if error? - if isDirectory - FileSystemImportManager.addFolder user_id, project_id, folder_id, name, path, replace, callback - else - FileTypeManager.getType name, path, (error, isBinary, charset) => - return callback(error) if error? - if isBinary - FileSystemImportManager.addFile user_id, project_id, folder_id, name, path, replace, (err, entity) -> - entity?.type = 'file' - callback(err, entity) - else - FileSystemImportManager.addDoc user_id, project_id, folder_id, name, path, charset, replace, (err, entity) -> - entity?.type = 'doc' - callback(err, entity) - - - _isSafeOnFileSystem: (path, callback = (err, isSafe)->)-> - fs.lstat path, (err, stat)-> - if err? - logger.err err:err, "error with path symlink check" - return callback(err) - isSafe = stat.isFile() or stat.isDirectory() - callback(err, isSafe) - diff --git a/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee b/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee deleted file mode 100644 index 46cb86cfc3..0000000000 --- a/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee +++ /dev/null @@ -1,70 +0,0 @@ -fs = require "fs" -Path = require("path") -isUtf8 = require('is-utf8'); - - -module.exports = FileTypeManager = - TEXT_EXTENSIONS : [ - "tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md", "asy", "latexmkrc", "lbx", "bbx", "cbx", "m" - ] - - IGNORE_EXTENSIONS : [ - "dvi", "aux", "log", "toc", "out", "pdfsync" - # Index and glossary files - "nlo", "ind", "glo", "gls", "glg" - # Bibtex - "bbl", "blg" - # Misc/bad - "doc", "docx", "gz" - ] - - IGNORE_FILENAMES : [ - "__MACOSX" - ".git" - ".gitignore" - ] - - MAX_TEXT_FILE_SIZE: 1 * 1024 * 1024 # 1 MB - - isDirectory: (path, callback = (error, result) ->) -> - fs.stat path, (error, stats) -> - return callback(error) if error? - callback(null, stats?.isDirectory()) - - # returns charset as understood by fs.readFile, - getType: (name, fsPath, callback = (error, isBinary, charset) ->) -> - parts = name.split(".") - extension = parts.slice(-1)[0].toLowerCase() - isText = (FileTypeManager.TEXT_EXTENSIONS.indexOf(extension) > -1 and parts.length > 1) or parts[0] == 'latexmkrc' - - return callback null, true unless isText - - fs.stat fsPath, (error, stat) -> - return callback(error) if error? - if stat.size > FileTypeManager.MAX_TEXT_FILE_SIZE - return callback null, true # Treat large text file as binary - - fs.readFile fsPath, (err, bytes) -> - return callback(err) if err? - - if isUtf8(bytes) - return callback null, false, "utf-8" - # check for little-endian unicode bom (nodejs does not support big-endian) - if bytes[0] == 0xFF and bytes[1] == 0xFE - return callback null, false, "utf-16le" - - callback null, false, "latin1" - - shouldIgnore: (path, callback = (error, result) ->) -> - name = Path.basename(path) - extension = name.split(".").slice(-1)[0] - if extension? - extension = extension.toLowerCase() - ignore = false - if name[0] == "." and extension != 'latexmkrc' - ignore = true - if @IGNORE_EXTENSIONS.indexOf(extension) != -1 - ignore = true - if @IGNORE_FILENAMES.indexOf(name) != -1 - ignore = true - callback null, ignore diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee deleted file mode 100644 index 86cd8985f6..0000000000 --- a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee +++ /dev/null @@ -1,81 +0,0 @@ -logger = require "logger-sharelatex" -metrics = require "metrics-sharelatex" -fs = require "fs" -Path = require "path" -FileSystemImportManager = require "./FileSystemImportManager" -ProjectUploadManager = require "./ProjectUploadManager" -AuthenticationController = require('../Authentication/AuthenticationController') -Settings = require "settings-sharelatex" -Errors = require "../Errors/Errors" -multer = require('multer') - -upload = null - -try - upload = multer( - dest: Settings.path.uploadFolder - limits: fileSize: Settings.maxUploadSize - ) -catch err - if err.message == "EEXIST" - logger.log uploadFolder:Settings.path.uploadFolder, "dir already exists, continuing" - else - logger.err err:err, "caught error from multer in uploads router" - -module.exports = ProjectUploadController = - uploadProject: (req, res, next) -> - timer = new metrics.Timer("project-upload") - user_id = AuthenticationController.getLoggedInUserId(req) - {originalname, path} = req.file - name = Path.basename(originalname, ".zip") - ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) -> - fs.unlink path, -> - timer.done() - if error? - logger.error - err: error, file_path: path, file_name: name, - "error uploading project" - if error.name? && error.name == 'InvalidError' - res.status(422).json { success: false, error: req.i18n.translate(error.message) } - else - res.status(500).json { success: false, error: req.i18n.translate("upload_failed") } - else - logger.log - project: project._id, file_path: path, file_name: name, - "uploaded project" - res.send success: true, project_id: project._id - - uploadFile: (req, res, next) -> - timer = new metrics.Timer("file-upload") - name = req.file?.originalname - path = req.file?.path - project_id = req.params.Project_id - folder_id = req.query.folder_id - if !name? or name.length == 0 or name.length > 150 - logger.err project_id:project_id, name:name, "bad name when trying to upload file" - return res.send success: false - logger.log folder_id:folder_id, project_id:project_id, "getting upload file request" - user_id = AuthenticationController.getLoggedInUserId(req) - - FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) -> - fs.unlink path, -> - timer.done() - if error? - logger.error - err: error, project_id: project_id, file_path: path, - file_name: name, folder_id: folder_id, - "error uploading file" - res.send success: false - else - logger.log - project_id: project_id, file_path: path, file_name: name, folder_id: folder_id - "uploaded file" - res.send success: true, entity_id: entity?._id, entity_type: entity?.type - - multerMiddleware: (req, res, next) -> - return res.status(500).json {success: false, error: req.i18n.translate("upload_failed")} unless upload? - upload.single('qqfile') req, res, (err) -> - if err instanceof multer.MulterError && err.code == 'LIMIT_FILE_SIZE' - return res.status(422).json {success: false, error: req.i18n.translate("file_too_large")} - - next(err) diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee deleted file mode 100644 index ecd6bb6ad6..0000000000 --- a/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee +++ /dev/null @@ -1,71 +0,0 @@ -path = require "path" -rimraf = require "rimraf" -async = require "async" -ArchiveManager = require "./ArchiveManager" -FileSystemImportManager = require "./FileSystemImportManager" -ProjectCreationHandler = require "../Project/ProjectCreationHandler" -ProjectRootDocManager = require "../Project/ProjectRootDocManager" -ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" -DocumentHelper = require "../Documents/DocumentHelper" - -module.exports = ProjectUploadHandler = - createProjectFromZipArchive: (owner_id, defaultName, zipPath, callback = (error, project) ->) -> - destination = @_getDestinationDirectory zipPath - docPath = null - project = null - - async.waterfall([ - (cb) -> - ArchiveManager.extractZipArchive zipPath, destination, cb - (cb) -> - ProjectRootDocManager.findRootDocFileFromDirectory destination, (error, _docPath, docContents) -> - cb(error, _docPath, docContents) - (_docPath, docContents, cb) -> - docPath = _docPath - proposedName = ProjectDetailsHandler.fixProjectName(DocumentHelper.getTitleFromTexContent(docContents || '') || defaultName) - ProjectDetailsHandler.generateUniqueName owner_id, proposedName, (error, name) -> - cb(error, name) - (name, cb) -> - ProjectCreationHandler.createBlankProject owner_id, name, (error, _project) -> - cb(error, _project) - (_project, cb) => - project = _project - @_insertZipContentsIntoFolder owner_id, project._id, project.rootFolder[0]._id, destination, cb - (cb) -> - if docPath? - ProjectRootDocManager.setRootDocFromName project._id, docPath, (error) -> - cb(error) - else - cb(null) - (cb) -> - cb(null, project) - ], callback) - - createProjectFromZipArchiveWithName: (owner_id, proposedName, zipPath, callback = (error, project) ->) -> - ProjectDetailsHandler.generateUniqueName owner_id, ProjectDetailsHandler.fixProjectName(proposedName), (error, name) => - return callback(error) if error? - ProjectCreationHandler.createBlankProject owner_id, name, (error, project) => - return callback(error) if error? - @insertZipArchiveIntoFolder owner_id, project._id, project.rootFolder[0]._id, zipPath, (error) -> - return callback(error) if error? - ProjectRootDocManager.setRootDocAutomatically project._id, (error) -> - return callback(error) if error? - callback(error, project) - - insertZipArchiveIntoFolder: (owner_id, project_id, folder_id, zipPath, callback = (error) ->) -> - destination = @_getDestinationDirectory zipPath - ArchiveManager.extractZipArchive zipPath, destination, (error) => - return callback(error) if error? - - @_insertZipContentsIntoFolder owner_id, project_id, folder_id, destination, callback - - _insertZipContentsIntoFolder: (owner_id, project_id, folder_id, destination, callback = (error) ->) -> - ArchiveManager.findTopLevelDirectory destination, (error, topLevelDestination) -> - return callback(error) if error? - FileSystemImportManager.addFolderContents owner_id, project_id, folder_id, topLevelDestination, false, (error) -> - return callback(error) if error? - rimraf(destination, callback) - - _getDestinationDirectory: (source) -> - return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}") - diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee deleted file mode 100644 index 6ffaa4212e..0000000000 --- a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee +++ /dev/null @@ -1,29 +0,0 @@ -AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') -AuthenticationController = require('../Authentication/AuthenticationController') -ProjectUploadController = require "./ProjectUploadController" -RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') -Settings = require('settings-sharelatex') - -module.exports = - apply: (webRouter, apiRouter) -> - webRouter.post '/project/new/upload', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: "project-upload" - maxRequests: 20 - timeInterval: 60 - }), - ProjectUploadController.multerMiddleware, - ProjectUploadController.uploadProject - - webRouter.post '/Project/:Project_id/upload', - RateLimiterMiddleware.rateLimit({ - endpointName: "file-upload" - params: ["Project_id"] - maxRequests: 200 - timeInterval: 60 * 30 - }), - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - ProjectUploadController.multerMiddleware, - ProjectUploadController.uploadFile diff --git a/services/web/app/coffee/Features/User/ThirdPartyIdentityManager.coffee b/services/web/app/coffee/Features/User/ThirdPartyIdentityManager.coffee deleted file mode 100644 index 9e5ad1ca2d..0000000000 --- a/services/web/app/coffee/Features/User/ThirdPartyIdentityManager.coffee +++ /dev/null @@ -1,91 +0,0 @@ -Errors = require "../Errors/Errors" -User = require("../../models/User").User -UserStub = require("../../models/UserStub").UserStub -UserUpdater = require "./UserUpdater" -_ = require "lodash" - -module.exports = ThirdPartyIdentityManager = - getUser: (providerId, externalUserId, callback) -> - return callback(new Error "invalid arguments") unless providerId? and externalUserId? - query = ThirdPartyIdentityManager._getUserQuery providerId, externalUserId - User.findOne query, (err, user) -> - return callback err if err? - return callback(new Errors.ThirdPartyUserNotFoundError()) unless user - callback null, user - - login: (providerId, externalUserId, externalData, callback) -> - ThirdPartyIdentityManager.getUser providerId, externalUserId, (err, user) -> - return callback err if err? - return callback(null, user) unless externalData - query = ThirdPartyIdentityManager._getUserQuery providerId, externalUserId - update = ThirdPartyIdentityManager._thirdPartyIdentifierUpdate user, providerId, externalUserId, externalData - User.findOneAndUpdate query, update, {new: true}, callback - - # attempt to login normally but check for user stub if user not found - loginUserStub: (providerId, externalUserId, externalData, callback) -> - ThirdPartyIdentityManager.login providerId, externalUserId, externalData, (err, user) -> - return callback null, user unless err? - return callback err unless err.name == "ThirdPartyUserNotFoundError" - query = ThirdPartyIdentityManager._getUserQuery providerId, externalUserId - UserStub.findOne query, (err, userStub) -> - return callback err if err? - return callback(new Errors.ThirdPartyUserNotFoundError()) unless userStub - return callback(null, userStub) unless externalData - update = ThirdPartyIdentityManager._thirdPartyIdentifierUpdate userStub, providerId, externalUserId, externalData - UserStub.findOneAndUpdate query, update, {new: true}, callback - - _getUserQuery: (providerId, externalUserId) -> - externalUserId = externalUserId.toString() - providerId = providerId.toString() - query = - "thirdPartyIdentifiers.externalUserId": externalUserId - "thirdPartyIdentifiers.providerId": providerId - return query - - _thirdPartyIdentifierUpdate: (user, providerId, externalUserId, externalData) -> - providerId = providerId.toString() - # get third party identifier object from array - thirdPartyIdentifier = user.thirdPartyIdentifiers.find (tpi) -> - tpi.externalUserId == externalUserId and tpi.providerId == providerId - # do recursive merge of new data over existing data - _.merge(thirdPartyIdentifier.externalData, externalData) - update = "thirdPartyIdentifiers.$": thirdPartyIdentifier - return update - - # register: () -> - # this should be implemented once we move to having v2 as the master - # but for now we need to register with v1 then call link once that - # is complete - - link: (user_id, providerId, externalUserId, externalData, callback, retry) -> - query = - _id: user_id - "thirdPartyIdentifiers.providerId": $ne: providerId - update = $push: thirdPartyIdentifiers: - externalUserId: externalUserId - externalData: externalData - providerId: providerId - # add new tpi only if an entry for the provider does not exist - UserUpdater.updateUser query, update, (err, res) -> - return callback err if err? - return callback null, res if res.nModified == 1 - # if already retried then throw error - return callback(new Error "update failed") if retry - # attempt to clear existing entry then retry - ThirdPartyIdentityManager.unlink user_id, providerId, (err) -> - return callback err if err? - ThirdPartyIdentityManager.link user_id, providerId, externalUserId, externalData, callback, true - - unlink: (user_id, providerId, callback) -> - update = $pull: thirdPartyIdentifiers: - providerId: providerId - UserUpdater.updateUser user_id, update, callback - - # attempt to unlink user but unlink user stub if not linked to user - unlinkUserStub: (user_id, providerId, callback) -> - ThirdPartyIdentityManager.unlink user_id, providerId, (err, res) -> - return callback err if err? - return callback null, res if res.nModified == 1 - update = $pull: thirdPartyIdentifiers: - providerId: providerId - UserStub.update { _id: user_id }, update, callback diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee deleted file mode 100644 index ab78878e4e..0000000000 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ /dev/null @@ -1,209 +0,0 @@ -UserHandler = require("./UserHandler") -UserDeleter = require("./UserDeleter") -UserGetter = require("./UserGetter") -User = require("../../models/User").User -newsLetterManager = require('../Newsletter/NewsletterManager') -UserRegistrationHandler = require("./UserRegistrationHandler") -logger = require("logger-sharelatex") -metrics = require("metrics-sharelatex") -Url = require("url") -AuthenticationManager = require("../Authentication/AuthenticationManager") -AuthenticationController = require('../Authentication/AuthenticationController') -UserSessionsManager = require("./UserSessionsManager") -UserUpdater = require("./UserUpdater") -SudoModeHandler = require('../SudoMode/SudoModeHandler') -settings = require "settings-sharelatex" -Errors = require "../Errors/Errors" - -module.exports = UserController = - - tryDeleteUser: (req, res, next) -> - UserController._tryDeleteUser(UserDeleter.deleteUser, req, res, next) - - trySoftDeleteUser: (req, res, next) -> - UserController._tryDeleteUser(UserDeleter.softDeleteUser, req, res, next) - - _tryDeleteUser: (deleteMethod, req, res, next) -> - user_id = AuthenticationController.getLoggedInUserId(req) - password = req.body.password - logger.log {user_id}, "trying to delete user account" - if !password? or password == '' - logger.err {user_id}, 'no password supplied for attempt to delete account' - return res.sendStatus(403) - AuthenticationManager.authenticate {_id: user_id}, password, (err, user) -> - if err? - logger.err {user_id}, 'error authenticating during attempt to delete account' - return next(err) - if !user - logger.err {user_id}, 'auth failed during attempt to delete account' - return res.sendStatus(403) - deleteMethod user_id, (err) -> - if err? - if err instanceof Errors.SubscriptionAdminDeletionError - return res.status(422).json(error: err.name) - else - logger.err {user_id}, "error while deleting user account" - return next(err) - sessionId = req.sessionID - req.logout?() - req.session.destroy (err) -> - if err? - logger.err err: err, 'error destorying session' - return next(err) - UserSessionsManager.untrackSession(user, sessionId) - res.sendStatus(200) - - unsubscribe: (req, res)-> - user_id = AuthenticationController.getLoggedInUserId(req) - UserGetter.getUser user_id, (err, user)-> - newsLetterManager.unsubscribe user, -> - res.send() - - updateUserSettings : (req, res)-> - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log user_id: user_id, "updating account settings" - User.findById user_id, (err, user)-> - if err? or !user? - logger.err err:err, user_id:user_id, "problem updaing user settings" - return res.sendStatus 500 - - if req.body.first_name? - user.first_name = req.body.first_name.trim() - if req.body.last_name? - user.last_name = req.body.last_name.trim() - if req.body.role? - user.role = req.body.role.trim() - if req.body.institution? - user.institution = req.body.institution.trim() - if req.body.mode? - user.ace.mode = req.body.mode - if req.body.editorTheme? - user.ace.theme = req.body.editorTheme - if req.body.overallTheme? - user.ace.overallTheme = req.body.overallTheme - if req.body.fontSize? - user.ace.fontSize = req.body.fontSize - if req.body.autoComplete? - user.ace.autoComplete = req.body.autoComplete - if req.body.autoPairDelimiters? - user.ace.autoPairDelimiters = req.body.autoPairDelimiters - if req.body.spellCheckLanguage? - user.ace.spellCheckLanguage = req.body.spellCheckLanguage - if req.body.pdfViewer? - user.ace.pdfViewer = req.body.pdfViewer - if req.body.syntaxValidation? - user.ace.syntaxValidation = req.body.syntaxValidation - if req.body.fontFamily? - user.ace.fontFamily = req.body.fontFamily - if req.body.lineHeight? - user.ace.lineHeight = req.body.lineHeight - - user.save (err)-> - newEmail = req.body.email?.trim().toLowerCase() - if !newEmail? or newEmail == user.email or req.externalAuthenticationSystemUsed() - # end here, don't update email - AuthenticationController.setInSessionUser(req, {first_name: user.first_name, last_name: user.last_name}) - return res.sendStatus 200 - else if newEmail.indexOf("@") == -1 - # email invalid - return res.sendStatus(400) - else - # update the user email - UserUpdater.changeEmailAddress user_id, newEmail, (err)-> - if err? - logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address" - if err instanceof Errors.EmailExistsError - message = req.i18n.translate("email_already_registered") - else - message = req.i18n.translate("problem_changing_email_address") - return res.send 500, {message:message} - User.findById user_id, (err, user)-> - if err? - logger.err err:err, user_id:user_id, "error getting user for email update" - return res.send 500 - AuthenticationController.setInSessionUser(req, {email: user.email, first_name: user.first_name, last_name: user.last_name}) - UserHandler.populateTeamInvites user, (err)-> #need to refresh this in the background - if err? - logger.err err:err, "error populateTeamInvites" - res.sendStatus(200) - - _doLogout: (req, cb = (err) ->) -> - metrics.inc "user.logout" - user = AuthenticationController.getSessionUser(req) - logger.log user: user, "logging out" - sessionId = req.sessionID - req.logout?() # passport logout - req.session.destroy (err)-> - if err - logger.err err: err, 'error destorying session' - cb(err) - if user? - UserSessionsManager.untrackSession(user, sessionId) - SudoModeHandler.clearSudoMode(user._id) - cb() - - logout : (req, res, next)-> - UserController._doLogout req, (err) -> - return next(err) if err? - redirect_url = if settings.overleaf? then settings.overleaf.host + '/users/ensure_signed_out' else '/login' - res.redirect redirect_url - - register : (req, res, next = (error) ->)-> - email = req.body.email - if !email? or email == "" - res.sendStatus 422 # Unprocessable Entity - return - UserRegistrationHandler.registerNewUserAndSendActivationEmail email, (error, user, setNewPasswordUrl) -> - return next(error) if error? - res.json { - email: user.email - setNewPasswordUrl: setNewPasswordUrl - } - - clearSessions: (req, res, next = (error) ->) -> - metrics.inc "user.clear-sessions" - user = AuthenticationController.getSessionUser(req) - logger.log {user_id: user._id}, "clearing sessions for user" - UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) -> - return next(err) if err? - res.sendStatus 201 - - changePassword : (req, res, next = (error) ->)-> - metrics.inc "user.password-change" - oldPass = req.body.currentPassword - user_id = AuthenticationController.getLoggedInUserId(req) - AuthenticationManager.authenticate {_id:user_id}, oldPass, (err, user)-> - return next(err) if err? - if(user) - logger.log user: user._id, "changing password" - newPassword1 = req.body.newPassword1 - newPassword2 = req.body.newPassword2 - validationError = AuthenticationManager.validatePassword(newPassword1) - if newPassword1 != newPassword2 - logger.log user: user, "passwords do not match" - res.send - message: - type:'error' - text:'Your passwords do not match' - else if validationError? - logger.log user: user, validationError.message - res.send - message: - type: 'error' - text: validationError.message - else - 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' - text:'Your password has been changed' - else - logger.log user_id: user_id, "current password wrong" - res.send - message: - type:'error' - text:'Your old password is wrong' diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee deleted file mode 100644 index 7624c57b87..0000000000 --- a/services/web/app/coffee/Features/User/UserCreator.coffee +++ /dev/null @@ -1,50 +0,0 @@ -User = require("../../models/User").User -logger = require("logger-sharelatex") -metrics = require('metrics-sharelatex') -{ addAffiliation } = require("../Institutions/InstitutionsAPI") - - -module.exports = UserCreator = - - createNewUser: (attributes, options, callback = (error, user) ->)-> - if arguments.length == 2 - callback = options - options = {} - logger.log user: attributes, "creating new user" - user = new User() - - username = attributes.email.match(/^[^@]*/) - if !attributes.first_name? or attributes.first_name == "" - attributes.first_name = username[0] - - for key, value of attributes - user[key] = value - - user.ace.syntaxValidation = true - user.featureSwitches?.pdfng = true - user.emails = [ - email: user.email - createdAt: new Date() - reversedHostname: user.email.split('@')[1].split('').reverse().join('') - ] - - user.save (err)-> - callback(err, user) - - return if options?.skip_affiliation - # call addaffiliation after the main callback so it runs in the - # background. There is no guaranty this will run so we must no rely on it - addAffiliation user._id, user.email, (error) -> - if error - logger.log { userId: user._id, email: user.email, error: error }, - "couldn't add affiliation for user on create" - else - logger.log { userId: user._id, email: user.email }, - "added affiliation for user on create" - - -metrics.timeAsyncMethod( - UserCreator, 'createNewUser', - 'mongo.UserCreator', - logger -) diff --git a/services/web/app/coffee/Features/User/UserDeleter.coffee b/services/web/app/coffee/Features/User/UserDeleter.coffee deleted file mode 100644 index 1a604b5e54..0000000000 --- a/services/web/app/coffee/Features/User/UserDeleter.coffee +++ /dev/null @@ -1,80 +0,0 @@ -User = require("../../models/User").User -NewsletterManager = require "../Newsletter/NewsletterManager" -ProjectDeleter = require("../Project/ProjectDeleter") -logger = require("logger-sharelatex") -SubscriptionHandler = require("../Subscription/SubscriptionHandler") -SubscriptionUpdater = require("../Subscription/SubscriptionUpdater") -SubscriptionLocator = require("../Subscription/SubscriptionLocator") -UserMembershipsHandler = require("../UserMembership/UserMembershipsHandler") -async = require("async") -InstitutionsAPI = require("../Institutions/InstitutionsAPI") -Errors = require("../Errors/Errors") -{db, ObjectId} = require("../../infrastructure/mongojs") - -module.exports = UserDeleter = - - softDeleteUserForMigration: (user_id, callback = (err)->)-> - if !user_id? - logger.err "user_id is null when trying to delete user" - return callback(new Error("no user_id")) - User.findById user_id, (err, user)-> - return callback(err) if err? - return callback(new Errors.NotFoundError("user not found")) unless user? - async.series([ - (cb) -> - UserDeleter._ensureCanDeleteUser user, cb - (cb) -> - UserDeleter._cleanupUser user, cb - (cb) -> - ProjectDeleter.deleteUsersProjects user._id, cb - (cb) -> - user.deletedAt = new Date() - db.usersDeletedByMigration.insert user, cb - (cb) -> - user.remove cb - ], callback) - - deleteUser: (user_id, callback = ()->)-> - if !user_id? - logger.err "user_id is null when trying to delete user" - return callback("no user_id") - User.findById user_id, (err, user)-> - if err? - return callback(err) - logger.log user:user, "deleting user" - async.series [ - (cb) -> - UserDeleter._ensureCanDeleteUser user, cb - (cb)-> - UserDeleter._cleanupUser user, cb - (cb)-> - ProjectDeleter.deleteUsersProjects user._id, cb - (cb)-> - user.remove cb - ], (err)-> - if err? - logger.err err:err, user_id:user_id, "something went wrong deleteing the user" - callback err - - _cleanupUser: (user, callback) -> - return callback(new Error("no user supplied")) unless user? - async.series([ - (cb)-> - NewsletterManager.unsubscribe user, (err) -> - logger.err("Failed to unsubscribe user from newsletter", user_id: user._id, error: err) - cb() - (cb)-> - SubscriptionHandler.cancelSubscription user, cb - (cb)-> - InstitutionsAPI.deleteAffiliations user._id, cb - (cb)-> - SubscriptionUpdater.removeUserFromAllGroups user._id, cb - (cb)-> - UserMembershipsHandler.removeUserFromAllEntities user._id, cb - ], callback) - - _ensureCanDeleteUser: (user, callback) -> - SubscriptionLocator.getUsersSubscription user, (error, subscription) -> - if subscription? - error ||= new Errors.SubscriptionAdminDeletionError() - callback(error) diff --git a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee b/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee deleted file mode 100644 index d353165e4d..0000000000 --- a/services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee +++ /dev/null @@ -1,47 +0,0 @@ -EmailHelper = require "../Helpers/EmailHelper" -EmailHandler = require "../Email/EmailHandler" -OneTimeTokenHandler = require "../Security/OneTimeTokenHandler" -settings = require 'settings-sharelatex' -Errors = require "../Errors/Errors" -logger = require "logger-sharelatex" -UserUpdater = require "./UserUpdater" -UserGetter = require "./UserGetter" - -ONE_YEAR_IN_S = 365 * 24 * 60 * 60 - -module.exports = UserEmailsConfirmationHandler = - sendConfirmationEmail: (user_id, email, emailTemplate, callback = (error) ->) -> - if arguments.length == 3 - callback = emailTemplate - emailTemplate = 'confirmEmail' - - # when force-migrating accounts to v2 from v1, we don't want to send confirmation messages - - # setting this env var allows us to turn this behaviour off - return callback(null) if process.env['SHARELATEX_NO_CONFIRMATION_MESSAGES']? - - email = EmailHelper.parseEmail(email) - return callback(new Error('invalid email')) if !email? - data = {user_id, email} - OneTimeTokenHandler.getNewToken 'email_confirmation', data, {expiresIn: ONE_YEAR_IN_S}, (err, token)-> - return callback(err) if err? - emailOptions = - to: email - confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}" - sendingUser_id: user_id - EmailHandler.sendEmail emailTemplate, emailOptions, callback - - confirmEmailFromToken: (token, callback = (error) ->) -> - logger.log {token_start: token.slice(0,8)}, 'confirming email from token' - OneTimeTokenHandler.getValueFromTokenAndExpire 'email_confirmation', token, (error, data) -> - return callback(error) if error? - if !data? - return callback(new Errors.NotFoundError('no token found')) - {user_id, email} = data - logger.log {data, user_id, email, token_start: token.slice(0,8)}, 'found data for email confirmation' - if !user_id? or email != EmailHelper.parseEmail(email) - return callback(new Errors.NotFoundError('invalid data')) - UserGetter.getUser user_id, {}, (error, user) -> - return callback(error) if error? - unless user?._id - return callback(new Errors.NotFoundError('user not found')) - UserUpdater.confirmEmail user_id, email, callback diff --git a/services/web/app/coffee/Features/User/UserEmailsController.coffee b/services/web/app/coffee/Features/User/UserEmailsController.coffee deleted file mode 100644 index e37a0452e7..0000000000 --- a/services/web/app/coffee/Features/User/UserEmailsController.coffee +++ /dev/null @@ -1,112 +0,0 @@ -AuthenticationController = require('../Authentication/AuthenticationController') -UserGetter = require("./UserGetter") -UserUpdater = require("./UserUpdater") -EmailHelper = require("../Helpers/EmailHelper") -UserEmailsConfirmationHandler = require "./UserEmailsConfirmationHandler" -{ endorseAffiliation } = require("../Institutions/InstitutionsAPI") -logger = require("logger-sharelatex") -Errors = require "../Errors/Errors" - -module.exports = UserEmailsController = - - list: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - UserGetter.getUserFullEmails userId, (error, fullEmails) -> - return next(error) if error? - res.json fullEmails - - - add: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - email = EmailHelper.parseEmail(req.body.email) - return res.sendStatus 422 unless email? - - affiliationOptions = - university: req.body.university - role: req.body.role - department: req.body.department - UserUpdater.addEmailAddress userId, email, affiliationOptions, (error)-> - if error? - return UserEmailsController._handleEmailError error, req, res, next - UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (err) -> - return next(error) if error? - res.sendStatus 204 - - - remove: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - email = EmailHelper.parseEmail(req.body.email) - return res.sendStatus 422 unless email? - - UserUpdater.removeEmailAddress userId, email, (error)-> - return next(error) if error? - res.sendStatus 200 - - - setDefault: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - email = EmailHelper.parseEmail(req.body.email) - return res.sendStatus 422 unless email? - - UserUpdater.updateV1AndSetDefaultEmailAddress userId, email, (error)-> - if error? - return UserEmailsController._handleEmailError error, req, res, next - else - return res.sendStatus 200 - - - endorse: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - email = EmailHelper.parseEmail(req.body.email) - return res.sendStatus 422 unless email? - - endorseAffiliation userId, email, req.body.role, req.body.department, (error)-> - return next(error) if error? - res.sendStatus 204 - - resendConfirmation: (req, res, next) -> - userId = AuthenticationController.getLoggedInUserId(req) - email = EmailHelper.parseEmail(req.body.email) - return res.sendStatus 422 unless email? - UserGetter.getUserByAnyEmail email, {_id:1}, (error, user) -> - return next(error) if error? - if !user? or user?._id?.toString() != userId - logger.log {userId, email, foundUserId: user?._id}, "email doesn't match logged in user" - return res.sendStatus 422 - logger.log {userId, email}, 'resending email confirmation token' - UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (error) -> - return next(error) if error? - res.sendStatus 200 - - showConfirm: (req, res, next) -> - res.render 'user/confirm_email', { - token: req.query.token, - title: 'confirm_email' - } - - confirm: (req, res, next) -> - token = req.body.token - if !token? - return res.sendStatus 422 - UserEmailsConfirmationHandler.confirmEmailFromToken token, (error) -> - if error? - if error instanceof Errors.NotFoundError - res.status(404).json({ - message: 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.' - }) - else - next(error) - else - res.sendStatus 200 - - _handleEmailError: (error, req, res, next) -> - if error instanceof Errors.UnconfirmedEmailError - return res.status(409).json { - message: 'email must be confirmed' - } - else if error instanceof Errors.EmailExistsError - return res.status(409).json { - message: req.i18n.translate("email_already_registered") - } - else - return next(error) \ No newline at end of file diff --git a/services/web/app/coffee/Features/User/UserGetter.coffee b/services/web/app/coffee/Features/User/UserGetter.coffee deleted file mode 100644 index e7d78fd747..0000000000 --- a/services/web/app/coffee/Features/User/UserGetter.coffee +++ /dev/null @@ -1,124 +0,0 @@ -mongojs = require("../../infrastructure/mongojs") -metrics = require('metrics-sharelatex') -logger = require('logger-sharelatex') -db = mongojs.db -ObjectId = mongojs.ObjectId -{ getUserAffiliations } = require("../Institutions/InstitutionsAPI") -Errors = require("../Errors/Errors") - -module.exports = UserGetter = - getUser: (query, projection, callback = (error, user) ->) -> - return callback(new Error("no query provided")) unless query? - if query?.email? - return callback(new Error("Don't use getUser to find user by email"), null) - if arguments.length == 2 - callback = projection - projection = {} - if typeof query == "string" - try - query = _id: ObjectId(query) - catch e - return callback(null, null) - else if query instanceof ObjectId - query = _id: query - - db.users.findOne query, projection, callback - - getUserEmail: (userId, callback = (error, email) ->) -> - @getUser userId, { email: 1 }, (error, user) -> - callback(error, user?.email) - - getUserFullEmails: (userId, callback = (error, emails) ->) -> - @getUser userId, { email: 1, emails: 1 }, (error, user) -> - return callback error if error? - return callback new Error('User not Found') unless user - - getUserAffiliations userId, (error, affiliationsData) -> - return callback error if error? - callback null, decorateFullEmails(user.email, user.emails or [], affiliationsData) - - getUserByMainEmail: (email, projection, callback = (error, user) ->) -> - email = email.trim() - if arguments.length == 2 - callback = projection - projection = {} - db.users.findOne email: email, projection, callback - - getUserByAnyEmail: (email, projection, callback = (error, user) ->) -> - email = email.trim() - if arguments.length == 2 - callback = projection - projection = {} - # $exists: true MUST be set to use the partial index - query = emails: { $exists: true }, 'emails.email': email - db.users.findOne query, projection, (error, user) => - return callback(error, user) if error? or user? - - # While multiple emails are being rolled out, check for the main email as - # well - @getUserByMainEmail email, projection, callback - - getUsersByAnyConfirmedEmail: (emails, projection, callback = (error, user) ->) -> - if arguments.length == 2 - callback = projection - projection = {} - # $exists: true MUST be set to use the partial index - query = emails: { $exists: true, $elemMatch: { email: { $in: emails }, confirmedAt: { $exists: true }}} - db.users.find query, projection, (error, users) => - callback(error, users) - - getUsersByHostname: (hostname, projection, callback = (error, users) ->) -> - reversedHostname = hostname.trim().split('').reverse().join('') - query = emails: { $exists: true }, 'emails.reversedHostname': reversedHostname - db.users.find query, projection, callback - - getUsers: (user_ids, projection, callback = (error, users) ->) -> - try - user_ids = user_ids.map (u) -> ObjectId(u.toString()) - catch error - return callback error - - db.users.find { _id: { $in: user_ids} }, projection, callback - - getUserOrUserStubById: (user_id, projection, callback = (error, user, isStub) ->) -> - try - query = _id: ObjectId(user_id.toString()) - catch e - return callback(new Error(e)) - db.users.findOne query, projection, (error, user) -> - return callback(error) if error? - return callback(null, user, false) if user? - db.userstubs.findOne query, projection, (error, user) -> - return callback(error) if error - return callback() if !user? - callback(null, user, true) - - # check for duplicate email address. This is also enforced at the DB level - ensureUniqueEmailAddress: (newEmail, callback) -> - @getUserByAnyEmail newEmail, (error, user) -> - return callback(new Errors.EmailExistsError('alread_exists')) if user? - callback(error) - -decorateFullEmails = (defaultEmail, emailsData, affiliationsData) -> - emailsData.map (emailData) -> - emailData.default = emailData.email == defaultEmail - - affiliation = affiliationsData.find (aff) -> aff.email == emailData.email - if affiliation? - { institution, inferred, role, department } = affiliation - emailData.affiliation = { institution, inferred, role, department } - else - emailsData.affiliation = null - - emailData - -[ - 'getUser', - 'getUserEmail', - 'getUserByMainEmail', - 'getUserByAnyEmail', - 'getUsers', - 'getUserOrUserStubById', - 'ensureUniqueEmailAddress', -].map (method) -> - metrics.timeAsyncMethod UserGetter, method, 'mongo.UserGetter', logger diff --git a/services/web/app/coffee/Features/User/UserHandler.coffee b/services/web/app/coffee/Features/User/UserHandler.coffee deleted file mode 100644 index b3e62f79a0..0000000000 --- a/services/web/app/coffee/Features/User/UserHandler.coffee +++ /dev/null @@ -1,8 +0,0 @@ -TeamInvitesHandler = require("../Subscription/TeamInvitesHandler") - -module.exports = UserHandler = - populateTeamInvites: (user, callback) -> - TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail(user.email, callback) - - setupLoginData: (user, callback = ->)-> - @populateTeamInvites user, callback diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee deleted file mode 100644 index 659c17416a..0000000000 --- a/services/web/app/coffee/Features/User/UserInfoController.coffee +++ /dev/null @@ -1,49 +0,0 @@ -UserGetter = require "./UserGetter" -logger = require("logger-sharelatex") -UserDeleter = require("./UserDeleter") -UserUpdater = require("./UserUpdater") -sanitize = require('sanitizer') -AuthenticationController = require('../Authentication/AuthenticationController') -ObjectId = require("mongojs").ObjectId - -module.exports = UserController = - getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) -> - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log user_id: user_id, "reciving request for getting logged in users personal info" - return next(new Error("User is not logged in")) if !user_id? - UserGetter.getUser user_id, { - first_name: true, last_name: true, - role:true, institution:true, - email: true, signUpDate: true - }, (error, user) -> - return next(error) if error? - UserController.sendFormattedPersonalInfo(user, res, next) - - getPersonalInfo: (req, res, next = (error) ->) -> - {user_id} = req.params - - if /^\d+$/.test(user_id) - query = { "overleaf.id": parseInt(user_id, 10) } - else if /^[a-f0-9]{24}$/.test(user_id) - query = { _id: ObjectId(user_id) } - else - return res.send(400) - - UserGetter.getUser query, { _id: true, first_name: true, last_name: true, email: true}, (error, user) -> - logger.log user_id: req.params.user_id, "receiving request for getting users personal info" - return next(error) if error? - return res.send(404) if !user? - UserController.sendFormattedPersonalInfo(user, res, next) - - sendFormattedPersonalInfo: (user, res, next = (error) ->) -> - info = UserController.formatPersonalInfo(user) - res.json info - - formatPersonalInfo: (user, callback = (error, info) ->) -> - if !user? - return {} - formatted_user = { id: user._id.toString() } - for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"] - if user[key]? - formatted_user[key] = user[key] - return formatted_user diff --git a/services/web/app/coffee/Features/User/UserInfoManager.coffee b/services/web/app/coffee/Features/User/UserInfoManager.coffee deleted file mode 100644 index 90971e31a5..0000000000 --- a/services/web/app/coffee/Features/User/UserInfoManager.coffee +++ /dev/null @@ -1,5 +0,0 @@ -UserGetter = require "./UserGetter" - -module.exports = UserInfoManager = - getPersonalInfo: (user_id, callback = (error) ->) -> - UserGetter.getUser user_id, { _id: true, first_name: true, last_name: true, email: true }, callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee deleted file mode 100644 index 70bda3596d..0000000000 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ /dev/null @@ -1,135 +0,0 @@ -UserGetter = require("./UserGetter") -UserSessionsManager = require("./UserSessionsManager") -ErrorController = require("../Errors/ErrorController") -logger = require("logger-sharelatex") -Settings = require("settings-sharelatex") -request = require 'request' -fs = require('fs') -AuthenticationController = require('../Authentication/AuthenticationController') - -module.exports = UserPagesController = - - registerPage : (req, res)-> - sharedProjectData = - project_name:req.query.project_name - user_first_name:req.query.user_first_name - - newTemplateData = {} - if req.session.templateData? - newTemplateData.templateName = req.session.templateData.templateName - - res.render 'user/register', - title: 'register' - sharedProjectData: sharedProjectData - newTemplateData: newTemplateData - new_email:req.query.new_email || "" - - 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) - - UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) -> - return next(error) if error? - 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. - res.redirect "/login?email=#{encodeURIComponent(user.email)}" - else - res.render 'user/activate', - title: 'activate_account' - email: user.email, - token: req.query.token - - loginPage : (req, res)-> - # if user is being sent to /login with explicit redirect (redir=/foo), - # such as being sent from the editor to /login, then set the redirect explicitly - if req.query.redir? and !AuthenticationController._getRedirectFromSession(req)? - logger.log {redir: req.query.redir}, "setting explicit redirect from login page" - AuthenticationController.setRedirectInSession(req, req.query.redir) - res.render 'user/login', - title: 'login', - email: req.query.email - - logoutPage: (req, res) -> - res.render 'user/logout' - - renderReconfirmAccountPage: (req, res) -> - page_data = { - reconfirm_email: req?.session?.reconfirm_email - } - # when a user must reconfirm their account - res.render 'user/reconfirm', page_data - - settingsPage : (req, res, next)-> - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log user: user_id, "loading settings page" - shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin) - oauthProviders = Settings.oauthProviders || {} - - UserGetter.getUser user_id, (err, user)-> - return next(err) if err? - - UserPagesController._hasPassword user, (err, passwordPresent) -> - if err - logger.err {err}, "error getting password status from v1" - res.render 'user/settings', - title:'account_settings' - user: user, - hasPassword: passwordPresent, - shouldAllowEditingDetails: shouldAllowEditingDetails - languages: Settings.languages, - accountSettingsTabActive: true, - oauthProviders: UserPagesController._translateProviderDescriptions(oauthProviders, req), - thirdPartyIds: UserPagesController._restructureThirdPartyIds(user), - previewOauth: req.query.prvw? - - sessionsPage: (req, res, next) -> - user = AuthenticationController.getSessionUser(req) - logger.log user_id: user._id, "loading sessions page" - UserSessionsManager.getAllUserSessions user, [req.sessionID], (err, sessions) -> - if err? - logger.err {user_id: user._id}, "error getting all user sessions" - return next(err) - res.render 'user/sessions', - title: "sessions" - sessions: sessions - - _hasPassword: (user, callback) -> - request.get { - url: "#{Settings.apis.v1.url}/api/v1/sharelatex/has_password" - auth: { user: Settings.apis.v1.user, pass: Settings.apis.v1.pass } - body: { user_id: user?.overleaf?.id } - timeout: 20 * 1000 - json: true - }, (err, response, body) -> - if err - # for errors assume password and show password setting form - return callback(err, true) - else if body?.has_password - return callback(err, true) - return callback(err, false) - - _restructureThirdPartyIds: (user) -> - # 3rd party identifiers are an array of objects - # this turn them into a single object, which - # makes data easier to use in template - return null if !user.thirdPartyIdentifiers || user.thirdPartyIdentifiers.length == 0 - user.thirdPartyIdentifiers.reduce (obj, identifier) -> - obj[identifier.providerId] = identifier.externalUserId - obj - , {} - - _translateProviderDescriptions: (providers, req) -> - result = {} - if providers - for provider, data of providers - data.description = req.i18n.translate(data.descriptionKey, data.descriptionOptions) - result[provider] = data - return result \ No newline at end of file diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee deleted file mode 100644 index 52d731c4bc..0000000000 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ /dev/null @@ -1,84 +0,0 @@ -User = require("../../models/User").User -UserCreator = require("./UserCreator") -UserGetter = require("./UserGetter") -AuthenticationManager = require("../Authentication/AuthenticationManager") -NewsLetterManager = require("../Newsletter/NewsletterManager") -async = require("async") -logger = require("logger-sharelatex") -crypto = require("crypto") -EmailHandler = require("../Email/EmailHandler") -OneTimeTokenHandler = require "../Security/OneTimeTokenHandler" -Analytics = require "../Analytics/AnalyticsManager" -settings = require "settings-sharelatex" -EmailHelper = require("../Helpers/EmailHelper") - -module.exports = UserRegistrationHandler = - _registrationRequestIsValid : (body, callback)-> - invalidEmail = AuthenticationManager.validateEmail(body.email or '') - invalidPassword = AuthenticationManager.validatePassword(body.password or '') - if invalidEmail? or invalidPassword? - return false - else - return true - - _createNewUserIfRequired: (user, userDetails, callback)-> - if !user? - 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 - - registerNewUser: (userDetails, callback)-> - self = @ - requestIsValid = @_registrationRequestIsValid userDetails - if !requestIsValid - return callback(new Error("request is not valid")) - userDetails.email = EmailHelper.parseEmail(userDetails.email) - UserGetter.getUserByAnyEmail userDetails.email, (err, user) => - if err? - return callback err - if user?.holdingAccount == false - return callback(new Error("EmailAlreadyRegistered"), user) - self._createNewUserIfRequired user, userDetails, (err, user)-> - if err? - return callback(err) - async.series [ - (cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb - (cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb - (cb)-> - if userDetails.subscribeToNewsletter == "true" - NewsLetterManager.subscribe user, -> - cb() #this can be slow, just fire it off - ], (err)-> - logger.log user: user, "registered" - Analytics.recordEvent user._id, "user-registered" - callback(err, user) - - registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) -> - logger.log {email}, "registering new user" - UserRegistrationHandler.registerNewUser { - email: email - password: crypto.randomBytes(32).toString("hex") - }, (err, user)-> - if err? and err?.message != "EmailAlreadyRegistered" - return callback(err) - - if err?.message == "EmailAlreadyRegistered" - logger.log {email}, "user already exists, resending welcome email" - - ONE_WEEK = 7 * 24 * 60 * 60 # seconds - OneTimeTokenHandler.getNewToken 'password', user._id, { expiresIn: ONE_WEEK }, (err, token)-> - return callback(err) if err? - - setNewPasswordUrl = "#{settings.siteUrl}/user/activate?token=#{token}&user_id=#{user._id}" - - EmailHandler.sendEmail "registered", { - to: user.email - setNewPasswordUrl: setNewPasswordUrl - }, () -> - - callback null, user, setNewPasswordUrl - - - - diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee deleted file mode 100644 index 57590a42b4..0000000000 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ /dev/null @@ -1,157 +0,0 @@ -Settings = require('settings-sharelatex') -logger = require("logger-sharelatex") -Async = require('async') -_ = require('underscore') -UserSessionsRedis = require('./UserSessionsRedis') -rclient = UserSessionsRedis.client() - -module.exports = UserSessionsManager = - # 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 = UserSessionsRedis.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 = UserSessionsRedis.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() - - getAllUserSessions: (user, exclude, callback=(err, sessionKeys)->) -> - exclude = _.map(exclude, UserSessionsManager._sessionKey) - sessionSetKey = UserSessionsRedis.sessionSetKey(user) - rclient.smembers sessionSetKey, (err, sessionKeys) -> - if err? - logger.err user_id: user._id, "error getting all session keys for user from redis" - return callback(err) - sessionKeys = _.filter sessionKeys, (k) -> !(_.contains(exclude, k)) - if sessionKeys.length == 0 - logger.log {user_id: user._id}, "no other sessions found, returning" - return callback(null, []) - - Async.mapSeries sessionKeys, ((k, cb) -> rclient.get(k, cb)), (err, sessions) -> - if err? - logger.err {user_id: user._id}, "error getting all sessions for user from redis" - return callback(err) - - result = [] - for session in sessions - if session is null - continue - session = JSON.parse(session) - session_user = session?.user or session?.passport?.user - result.push { - ip_address: session_user.ip_address, - session_created: session_user.session_created - } - - return callback(null, result) - - 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 = UserSessionsRedis.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" - - deletions = keysToDelete.map (k) -> - (cb) -> - rclient.del k, cb - - Async.series deletions, (err, _result) -> - if err? - logger.err {err, user_id: user._id, sessionSetKey}, "errror revoking all sessions for user" - return callback(err) - rclient.srem sessionSetKey, keysToDelete, (err) -> - if err? - logger.err {err, user_id: user._id, sessionSetKey}, "error removing session set 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 = UserSessionsRedis.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 = UserSessionsRedis.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) - ) diff --git a/services/web/app/coffee/Features/User/UserSessionsRedis.coffee b/services/web/app/coffee/Features/User/UserSessionsRedis.coffee deleted file mode 100644 index 0c460b4604..0000000000 --- a/services/web/app/coffee/Features/User/UserSessionsRedis.coffee +++ /dev/null @@ -1,9 +0,0 @@ -RedisWrapper = require("../../infrastructure/RedisWrapper") -rclient = RedisWrapper.client("websessions") - -module.exports = Redis = - client: () -> - return rclient - - sessionSetKey: (user) -> - return "UserSessions:{#{user._id}}" diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee deleted file mode 100644 index 05e37b9870..0000000000 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ /dev/null @@ -1,203 +0,0 @@ -logger = require("logger-sharelatex") -mongojs = require("../../infrastructure/mongojs") -metrics = require("metrics-sharelatex") -db = mongojs.db -async = require("async") -ObjectId = mongojs.ObjectId -UserGetter = require("./UserGetter") -{ addAffiliation, removeAffiliation } = require("../Institutions/InstitutionsAPI") -FeaturesUpdater = require("../Subscription/FeaturesUpdater") -EmailHelper = require "../Helpers/EmailHelper" -Errors = require "../Errors/Errors" -Settings = require "settings-sharelatex" -request = require 'request' -NewsletterManager = require "../Newsletter/NewsletterManager" - -module.exports = UserUpdater = - updateUser: (query, update, callback = (error) ->) -> - if typeof query == "string" - query = _id: ObjectId(query) - else if query instanceof ObjectId - query = _id: query - else if typeof query._id == "string" - query._id = ObjectId(query._id) - - db.users.update query, update, callback - - - # - # DEPRECATED - # - # Change the user's main email address by adding a new email, switching the - # default email and removing the old email. Prefer manipulating multiple - # emails and the default rather than calling this method directly - # - changeEmailAddress: (userId, newEmail, callback)-> - newEmail = EmailHelper.parseEmail(newEmail) - return callback(new Error('invalid email')) if !newEmail? - logger.log userId: userId, newEmail: newEmail, "updaing email address of user" - - oldEmail = null - async.series [ - (cb) -> - UserGetter.getUserEmail userId, (error, email) -> - oldEmail = email - cb(error) - (cb) -> UserUpdater.addEmailAddress userId, newEmail, cb - (cb) -> UserUpdater.setDefaultEmailAddress userId, newEmail, cb - (cb) -> UserUpdater.removeEmailAddress userId, oldEmail, cb - ], callback - - - # Add a new email address for the user. Email cannot be already used by this - # or any other user - addEmailAddress: (userId, newEmail, affiliationOptions, callback) -> - unless callback? # affiliationOptions is optional - callback = affiliationOptions - affiliationOptions = {} - newEmail = EmailHelper.parseEmail(newEmail) - return callback(new Error('invalid email')) if !newEmail? - - UserGetter.ensureUniqueEmailAddress newEmail, (error) => - return callback(error) if error? - - addAffiliation userId, newEmail, affiliationOptions, (error) => - if error? - logger.err error: error, 'problem adding affiliation while adding email' - return callback(error) - - reversedHostname = newEmail.split('@')[1].split('').reverse().join('') - update = $push: emails: email: newEmail, createdAt: new Date(), reversedHostname: reversedHostname - @updateUser userId, update, (error) -> - if error? - logger.err error: error, 'problem updating users emails' - return callback(error) - callback() - - - # remove one of the user's email addresses. The email cannot be the user's - # default email address - removeEmailAddress: (userId, email, callback) -> - email = EmailHelper.parseEmail(email) - return callback(new Error('invalid email')) if !email? - removeAffiliation userId, email, (error) => - if error? - logger.err error: error, 'problem removing affiliation' - return callback(error) - - query = _id: userId, email: $ne: email - update = $pull: emails: email: email - @updateUser query, update, (error, res) -> - if error? - logger.err error:error, 'problem removing users email' - return callback(error) - if res.n == 0 - return callback(new Error('Cannot remove email')) - callback() - - - # set the default email address by setting the `email` attribute. The email - # must be one of the user's multiple emails (`emails` attribute) - setDefaultEmailAddress: (userId, email, callback) -> - email = EmailHelper.parseEmail(email) - return callback(new Error('invalid email')) if !email? - UserGetter.getUserEmail userId, (error, oldEmail) => - if err? - return callback(error) - query = _id: userId, 'emails.email': email - update = $set: email: email - @updateUser query, update, (error, res) -> - if error? - logger.err error:error, 'problem setting default emails' - return callback(error) - else if res.n == 0 # TODO: Check n or nMatched? - return callback(new Error('Default email does not belong to user')) - else - NewsletterManager.changeEmail oldEmail, email, -> - callback() - - - - updateV1AndSetDefaultEmailAddress: (userId, email, callback) -> - @updateEmailAddressInV1 userId, email, (error) => - return callback(error) if error? - @setDefaultEmailAddress userId, email, callback - - updateEmailAddressInV1: (userId, newEmail, callback) -> - if !Settings.apis?.v1?.url? - return callback() - UserGetter.getUser userId, { 'overleaf.id': 1, emails: 1 }, (error, user) -> - return callback(error) if error? - return callback(new Errors.NotFoundError('no user found')) if !user? - if !user.overleaf?.id? - return callback() - newEmailIsConfirmed = false - for email in user.emails - if email.email == newEmail and email.confirmedAt? - newEmailIsConfirmed = true - break - if !newEmailIsConfirmed - return callback(new Errors.UnconfirmedEmailError("can't update v1 with unconfirmed email")) - request { - baseUrl: Settings.apis.v1.url - url: "/api/v1/sharelatex/users/#{user.overleaf.id}/email" - method: 'PUT' - auth: - user: Settings.apis.v1.user - pass: Settings.apis.v1.pass - sendImmediately: true - json: - user: - email: newEmail - timeout: 5 * 1000 - }, (error, response, body) -> - if error? - error = new Errors.V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED' - return callback(error) - if response.statusCode == 409 # Conflict - return callback(new Errors.EmailExistsError('email exists in v1')) - else if 200 <= response.statusCode < 300 - return callback() - else - return callback new Error("non-success code from v1: #{response.statusCode}") - - confirmEmail: (userId, email, confirmedAt, callback) -> - if arguments.length == 3 - callback = confirmedAt - confirmedAt = new Date() - email = EmailHelper.parseEmail(email) - return callback(new Error('invalid email')) if !email? - logger.log {userId, email}, 'confirming user email' - addAffiliation userId, email, {confirmedAt: confirmedAt}, (error) => - if error? - logger.err error: error, 'problem adding affiliation while confirming email' - return callback(error) - - query = - _id: userId - 'emails.email': email - update = - $set: - 'emails.$.confirmedAt': confirmedAt - @updateUser query, update, (error, res) -> - return callback(error) if error? - logger.log {res, userId, email}, "tried to confirm email" - if res.n == 0 - return callback(new Errors.NotFoundError('user id and email do no match')) - FeaturesUpdater.refreshFeatures userId, true, callback - - removeReconfirmFlag: (user_id, callback) -> - UserUpdater.updateUser user_id.toString(), { - $set: { "must_reconfirm": false } - }, (error) -> - callback(error) - -[ - 'updateUser' - 'changeEmailAddress' - 'setDefaultEmailAddress' - 'addEmailAddress' - 'removeEmailAddress' - 'removeReconfirmFlag' -].map (method) -> - metrics.timeAsyncMethod(UserUpdater, method, 'mongo.UserUpdater', logger) diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee deleted file mode 100644 index 5273942714..0000000000 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee +++ /dev/null @@ -1,125 +0,0 @@ -AuthenticationController = require('../Authentication/AuthenticationController') -AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') -UserMembershipHandler = require('./UserMembershipHandler') -EntityConfigs = require('./UserMembershipEntityConfigs') -Errors = require('../Errors/Errors') -logger = require("logger-sharelatex") -settings = require 'settings-sharelatex' -request = require 'request' - -module.exports = UserMembershipAuthorization = - requireTeamMetricsAccess: (req, res, next) -> - requireAccessToEntity('team', req.params.id, req, res, next, 'groupMetrics') - - requireGroupManagementAccess: (req, res, next) -> - requireAccessToEntity('group', req.params.id, req, res, next, 'groupManagement') - - requireGroupMetricsAccess: (req, res, next) -> - requireAccessToEntity('group', req.params.id, req, res, next, 'groupMetrics') - - requireGroupManagersManagementAccess: (req, res, next) -> - requireAccessToEntity('groupManagers', req.params.id, req, res, next, 'groupManagement') - - requireInstitutionMetricsAccess: (req, res, next) -> - requireAccessToEntity('institution', req.params.id, req, res, next, 'institutionMetrics') - - requireInstitutionManagementAccess: (req, res, next) -> - requireAccessToEntity('institution', req.params.id, req, res, next, 'institutionManagement') - - requirePublisherMetricsAccess: (req, res, next) -> - requireAccessToEntity('publisher', req.params.id, req, res, next, 'publisherMetrics') - - requirePublisherManagementAccess: (req, res, next) -> - requireAccessToEntity('publisher', req.params.id, req, res, next, 'publisherManagement') - - requireTemplateMetricsAccess: (req, res, next) -> - templateId = req.params.id - request { - baseUrl: settings.apis.v1.url - url: "/api/v2/templates/#{templateId}" - method: 'GET' - auth: - user: settings.apis.v1.user - pass: settings.apis.v1.pass - sendImmediately: true - }, (error, response, body) => - if response.statusCode == 404 - return next(new Errors.NotFoundError()) - - if response.statusCode != 200 - logger.err { templateId }, "[TemplateMetrics] Couldn't fetch template data from v1" - return next(new Error("Couldn't fetch template data from v1")) - - return next(error) if error? - try - body = JSON.parse(body) - catch error - return next(error) - - req.template = - id: body.id - title: body.title - if body?.brand?.slug - req.params.id = body.brand.slug - UserMembershipAuthorization.requirePublisherMetricsAccess(req, res, next) - else - AuthorizationMiddleware.ensureUserIsSiteAdmin(req, res, next) - - requireGraphAccess: (req, res, next) -> - req.params.id = req.query.resource_id - if req.query.resource_type == 'template' - return UserMembershipAuthorization.requireTemplateMetricsAccess(req, res, next) - else if req.query.resource_type == 'institution' - return UserMembershipAuthorization.requireInstitutionMetricsAccess(req, res, next) - else if req.query.resource_type == 'group' - return UserMembershipAuthorization.requireGroupMetricsAccess(req, res, next) - else if req.query.resource_type == 'team' - return UserMembershipAuthorization.requireTeamMetricsAccess(req, res, next) - requireAccessToEntity(req.query.resource_type, req.query.resource_id, req, res, next) - - requireEntityCreationAccess: (req, res, next) -> - loggedInUser = AuthenticationController.getSessionUser(req) - unless loggedInUser and hasEntityCreationAccess(loggedInUser) - return AuthorizationMiddleware.redirectToRestricted req, res, next - next() - -requireAccessToEntity = (entityName, entityId, req, res, next, requiredStaffAccess=null) -> - loggedInUser = AuthenticationController.getSessionUser(req) - unless loggedInUser - return AuthorizationMiddleware.redirectToRestricted req, res, next - - getEntity entityName, entityId, loggedInUser, requiredStaffAccess, (error, entity, entityConfig, entityExists) -> - return next(error) if error? - - if entity? - req.entity = entity - req.entityConfig = entityConfig - return next() - - if entityExists # user doesn't have access to entity - return AuthorizationMiddleware.redirectToRestricted(req, res, next) - - if hasEntityCreationAccess(loggedInUser) and entityConfig.canCreate - # entity doesn't exists, admin can create it - return res.redirect "/entities/#{entityName}/create/#{entityId}" - - next(new Errors.NotFoundError()) - -getEntity = (entityName, entityId, user, requiredStaffAccess, callback = (error, entity, entityConfig, entityExists)->) -> - entityConfig = EntityConfigs[entityName] - unless entityConfig - return callback(new Errors.NotFoundError("No such entity: #{entityName}")) - - UserMembershipHandler.getEntity entityId, entityConfig, user, requiredStaffAccess, (error, entity)-> - return callback(error) if error? - return callback(null, entity, entityConfig, true) if entity? - - # no access to entity. Check if entity exists - UserMembershipHandler.getEntityWithoutAuthorizationCheck entityId, entityConfig, (error, entity)-> - return callback(error) if error? - callback(null, null, entityConfig, entity?) - -hasEntityCreationAccess = (user) -> - user.isAdmin or - user.staffAccess?['institutionManagement'] or - user.staffAccess?['publisherManagement'] diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipController.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipController.coffee deleted file mode 100644 index 1aced1c2a6..0000000000 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipController.coffee +++ /dev/null @@ -1,100 +0,0 @@ -AuthenticationController = require('../Authentication/AuthenticationController') -UserMembershipHandler = require('./UserMembershipHandler') -EntityConfigs = require('./UserMembershipEntityConfigs') -Errors = require('../Errors/Errors') -EmailHelper = require("../Helpers/EmailHelper") -logger = require("logger-sharelatex") - -module.exports = - index: (req, res, next)-> - { entity, entityConfig } = req - entity.fetchV1Data (error, entity) -> - return next(error) if error? - UserMembershipHandler.getUsers entity, entityConfig, (error, users)-> - return next(error) if error? - entityPrimaryKey = entity[entityConfig.fields.primaryKey].toString() - entityName = entity[entityConfig.fields.name] if entityConfig.fields.name - res.render "user_membership/index", - name: entityName - users: users - groupSize: entity.membersLimit if entityConfig.hasMembersLimit - translations: entityConfig.translations - paths: entityConfig.pathsFor(entityPrimaryKey) - - add: (req, res, next)-> - { entity, entityConfig } = req - email = EmailHelper.parseEmail(req.body.email) - if !email? - return res.status(400).json error: - code: 'invalid_email' - message: req.i18n.translate('invalid_email') - - - if entityConfig.readOnly - return next(new Errors.NotFoundError("Cannot add users to entity")) - - UserMembershipHandler.addUser entity, entityConfig, email, (error, user)-> - if error?.alreadyAdded - return res.status(400).json error: - code: 'user_already_added' - message: req.i18n.translate('user_already_added') - if error?.userNotFound - return res.status(404).json error: - code: 'user_not_found' - message: req.i18n.translate('user_not_found') - return next(error) if error? - res.json(user: user) - - remove: (req, res, next)-> - { entity, entityConfig } = req - userId = req.params.userId - - if entityConfig.readOnly - return next(new Errors.NotFoundError("Cannot remove users from entity")) - - loggedInUserId = AuthenticationController.getLoggedInUserId(req) - if loggedInUserId == userId - return res.status(400).json error: - code: 'managers_cannot_remove_self' - message: req.i18n.translate('managers_cannot_remove_self') - - UserMembershipHandler.removeUser entity, entityConfig, userId, (error, user)-> - if error?.isAdmin - return res.status(400).json error: - code: 'managers_cannot_remove_admin' - message: req.i18n.translate('managers_cannot_remove_admin') - return next(error) if error? - res.send() - - exportCsv: (req, res, next)-> - { entity, entityConfig } = req - logger.log subscriptionId: entity._id, "exporting csv" - UserMembershipHandler.getUsers entity, entityConfig, (error, users)-> - return next(error) if error? - csvOutput = "" - for user in users - csvOutput += user.email + "\n" - res.header( - "Content-Disposition", - "attachment; filename=Group.csv" - ) - res.contentType('text/csv') - res.send(csvOutput) - - new: (req, res, next)-> - res.render "user_membership/new", - entityName: req.params.name - entityId: req.params.id - - create: (req, res, next)-> - entityName = req.params.name - entityId = req.params.id - entityConfig = EntityConfigs[entityName] - unless entityConfig - return next(new Errors.NotFoundError("No such entity: #{entityName}")) - unless entityConfig.canCreate - return next(new Errors.NotFoundError("Cannot create new #{entityName}")) - - UserMembershipHandler.createEntity entityId, entityConfig, (error, entity) -> - return next(error) if error? - res.redirect entityConfig.pathsFor(entityId).index diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee deleted file mode 100644 index b5e5b9a8b4..0000000000 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee +++ /dev/null @@ -1,84 +0,0 @@ -module.exports = - group: - modelName: 'Subscription' - readOnly: true - hasMembersLimit: true - fields: - primaryKey: '_id' - read: ['invited_emails', 'teamInvites', 'member_ids'] - write: null - access: 'manager_ids' - name: 'teamName' - baseQuery: - groupPlan: true - translations: - title: 'group_account' - subtitle: 'members_management' - remove: 'remove_from_group' - pathsFor: (id) -> - addMember: "/manage/groups/#{id}/invites" - removeMember: "/manage/groups/#{id}/user" - removeInvite: "/manage/groups/#{id}/invites" - exportMembers: "/manage/groups/#{id}/members/export" - - team: # for metrics only - modelName: 'Subscription' - fields: - primaryKey: 'overleaf.id' - access: 'manager_ids' - baseQuery: - groupPlan: true - - groupManagers: - modelName: 'Subscription' - fields: - primaryKey: '_id' - read: ['manager_ids'] - write: 'manager_ids' - access: 'manager_ids' - name: 'teamName' - baseQuery: - groupPlan: true - translations: - title: 'group_account' - subtitle: 'managers_management' - remove: 'remove_manager' - pathsFor: (id) -> - addMember: "/manage/groups/#{id}/managers" - removeMember: "/manage/groups/#{id}/managers" - - institution: - modelName: 'Institution' - canCreate: true - fields: - primaryKey: 'v1Id' - read: ['managerIds'] - write: 'managerIds' - access: 'managerIds' - name: 'name' - translations: - title: 'institution_account' - subtitle: 'managers_management' - remove: 'remove_manager' - pathsFor: (id) -> - index: "/manage/institutions/#{id}/managers" - addMember: "/manage/institutions/#{id}/managers" - removeMember: "/manage/institutions/#{id}/managers" - - publisher: - modelName: 'Publisher' - canCreate: true - fields: - primaryKey: 'slug' - read: ['managerIds'] - write: 'managerIds' - access: 'managerIds' - name: 'name' - translations: - title: 'publisher_account' - subtitle: 'managers_management' - remove: 'remove_manager' - pathsFor: (id) -> - index: "/manage/publishers/#{id}/managers" - addMember: "/manage/publishers/#{id}/managers" - removeMember: "/manage/publishers/#{id}/managers" diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee deleted file mode 100644 index fd8c30bb22..0000000000 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee +++ /dev/null @@ -1,77 +0,0 @@ -ObjectId = require('mongoose').Types.ObjectId -async = require("async") -Errors = require('../Errors/Errors') -EntityModels = - Institution: require('../../models/Institution').Institution - Subscription: require('../../models/Subscription').Subscription - Publisher: require('../../models/Publisher').Publisher -UserMembershipViewModel = require('./UserMembershipViewModel') -UserGetter = require('../User/UserGetter') -logger = require('logger-sharelatex') -UserMembershipEntityConfigs = require "./UserMembershipEntityConfigs" - -module.exports = - getEntity: (entityId, entityConfig, loggedInUser, requiredStaffAccess, callback = (error, entity) ->) -> - query = buildEntityQuery(entityId, entityConfig) - unless loggedInUser.isAdmin or loggedInUser.staffAccess?[requiredStaffAccess] - query[entityConfig.fields.access] = ObjectId(loggedInUser._id) - EntityModels[entityConfig.modelName].findOne query, callback - - getEntityWithoutAuthorizationCheck: (entityId, entityConfig, callback = (error, entity) ->) -> - query = buildEntityQuery(entityId, entityConfig) - EntityModels[entityConfig.modelName].findOne query, callback - - createEntity: (entityId, entityConfig, callback = (error, entity) ->) -> - data = buildEntityQuery(entityId, entityConfig) - EntityModels[entityConfig.modelName].create data, callback - - getUsers: (entity, entityConfig, callback = (error, users) ->) -> - attributes = entityConfig.fields.read - getPopulatedListOfMembers(entity, attributes, callback) - - addUser: (entity, entityConfig, email, callback = (error, user) ->) -> - attribute = entityConfig.fields.write - UserGetter.getUserByAnyEmail email, (error, user) -> - return callback(error) if error? - unless user - return callback(userNotFound: true) - if entity[attribute].some((managerId) -> managerId.equals(user._id)) - return callback(alreadyAdded: true) - - addUserToEntity entity, attribute, user, (error) -> - callback(error, UserMembershipViewModel.build(user)) - - removeUser: (entity, entityConfig, userId, callback = (error) ->) -> - attribute = entityConfig.fields.write - if entity.admin_id?.equals(userId) - return callback(isAdmin: true) - removeUserFromEntity entity, attribute, userId, callback - -getPopulatedListOfMembers = (entity, attributes, callback = (error, users)->)-> - userObjects = [] - - for attribute in attributes - for userObject in entity[attribute] or [] - # userObject can be an email as String, a user id as ObjectId or an - # invite as Object with an email attribute as String. We want to pass to - # UserMembershipViewModel either an email as (String) or a user id (ObjectId) - userIdOrEmail = userObject.email || userObject - userObjects.push userIdOrEmail - - async.map userObjects, UserMembershipViewModel.buildAsync, callback - -addUserToEntity = (entity, attribute, user, callback = (error)->) -> - fieldUpdate = {} - fieldUpdate[attribute] = user._id - entity.update { $addToSet: fieldUpdate }, callback - -removeUserFromEntity = (entity, attribute, userId, callback = (error)->) -> - fieldUpdate = {} - fieldUpdate[attribute] = userId - entity.update { $pull: fieldUpdate }, callback - -buildEntityQuery = (entityId, entityConfig, loggedInUser) -> - entityId = ObjectId(entityId) if ObjectId.isValid(entityId.toString()) - query = Object.assign({}, entityConfig.baseQuery) - query[entityConfig.fields.primaryKey] = entityId - query diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee deleted file mode 100644 index 5f05d9281b..0000000000 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee +++ /dev/null @@ -1,75 +0,0 @@ -UserMembershipAuthorization = require './UserMembershipAuthorization' -UserMembershipController = require './UserMembershipController' -SubscriptionGroupController = require '../Subscription/SubscriptionGroupController' -TeamInvitesController = require '../Subscription/TeamInvitesController' -RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') - -module.exports = - apply: (webRouter) -> - # group members routes - webRouter.get '/manage/groups/:id/members', - UserMembershipAuthorization.requireGroupManagementAccess, - UserMembershipController.index - webRouter.post '/manage/groups/:id/invites', - UserMembershipAuthorization.requireGroupManagementAccess, - RateLimiterMiddleware.rateLimit({ - endpointName: "create-team-invite" - maxRequests: 100 - timeInterval: 60 - }), - TeamInvitesController.createInvite - webRouter.delete '/manage/groups/:id/user/:user_id', - UserMembershipAuthorization.requireGroupManagementAccess, - SubscriptionGroupController.removeUserFromGroup - webRouter.delete '/manage/groups/:id/invites/:email', - UserMembershipAuthorization.requireGroupManagementAccess, - TeamInvitesController.revokeInvite - webRouter.get '/manage/groups/:id/members/export', - UserMembershipAuthorization.requireGroupManagementAccess, - RateLimiterMiddleware.rateLimit({ - endpointName: "export-team-csv" - maxRequests: 30 - timeInterval: 60 - }), - UserMembershipController.exportCsv - - # group managers routes - webRouter.get "/manage/groups/:id/managers", - UserMembershipAuthorization.requireGroupManagersManagementAccess, - UserMembershipController.index - webRouter.post "/manage/groups/:id/managers", - UserMembershipAuthorization.requireGroupManagersManagementAccess, - UserMembershipController.add - webRouter.delete "/manage/groups/:id/managers/:userId", - UserMembershipAuthorization.requireGroupManagersManagementAccess, - UserMembershipController.remove - - # institution members routes - webRouter.get "/manage/institutions/:id/managers", - UserMembershipAuthorization.requireInstitutionManagementAccess, - UserMembershipController.index - webRouter.post "/manage/institutions/:id/managers", - UserMembershipAuthorization.requireInstitutionManagementAccess, - UserMembershipController.add - webRouter.delete "/manage/institutions/:id/managers/:userId", - UserMembershipAuthorization.requireInstitutionManagementAccess, - UserMembershipController.remove - - # publisher members routes - webRouter.get "/manage/publishers/:id/managers", - UserMembershipAuthorization.requirePublisherManagementAccess, - UserMembershipController.index - webRouter.post "/manage/publishers/:id/managers", - UserMembershipAuthorization.requirePublisherManagementAccess, - UserMembershipController.add - webRouter.delete "/manage/publishers/:id/managers/:userId", - UserMembershipAuthorization.requirePublisherManagementAccess, - UserMembershipController.remove - - # create new entitites - webRouter.get "/entities/:name/create/:id", - UserMembershipAuthorization.requireEntityCreationAccess, - UserMembershipController.new - webRouter.post "/entities/:name/create/:id", - UserMembershipAuthorization.requireEntityCreationAccess, - UserMembershipController.create diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipViewModel.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipViewModel.coffee deleted file mode 100644 index ac5ba42168..0000000000 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipViewModel.coffee +++ /dev/null @@ -1,45 +0,0 @@ -ObjectId = require('mongojs').ObjectId -UserGetter = require('../User/UserGetter') - -module.exports = UserMembershipViewModel = - build: (userOrEmail) -> - if userOrEmail._id - buildUserViewModel userOrEmail - else - buildUserViewModelWithEmail userOrEmail - - - buildAsync: (userOrIdOrEmail, callback = (error, viewModel)->) -> - unless userOrIdOrEmail instanceof ObjectId - # userOrIdOrEmail is a user or an email and can be parsed by #build - return callback(null, UserMembershipViewModel.build(userOrIdOrEmail)) - - userId = userOrIdOrEmail - projection = { email: 1, first_name: 1, last_name: 1 } - UserGetter.getUserOrUserStubById userId, projection, (error, user, isStub) -> - if error? or !user? - return callback(null, buildUserViewModelWithId(userId.toString())) - if isStub - return callback(null, buildUserViewModelWithStub(user)) - callback(null, buildUserViewModel(user)) - - -buildUserViewModel = (user, isInvite = false) -> - _id: user._id or null - email: user.email or null - first_name: user.first_name or null - last_name: user.last_name or null - invite: isInvite - - -buildUserViewModelWithEmail = (email) -> - buildUserViewModel({ email }, true) - - -buildUserViewModelWithStub = (user) -> - # user stubs behave as invites - buildUserViewModel(user, true) - - -buildUserViewModelWithId = (id) -> - buildUserViewModel({ _id: id }, false) diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipsHandler.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipsHandler.coffee deleted file mode 100644 index 41d3f5ef0f..0000000000 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipsHandler.coffee +++ /dev/null @@ -1,32 +0,0 @@ -async = require("async") -EntityModels = - Institution: require('../../models/Institution').Institution - Subscription: require('../../models/Subscription').Subscription - Publisher: require('../../models/Publisher').Publisher -UserMembershipEntityConfigs = require "./UserMembershipEntityConfigs" - -module.exports = UserMembershipsHandler = - removeUserFromAllEntities: (userId, callback = (error) ->) -> - # get all writable entity types - entityConfigs = [] - for key, entityConfig of UserMembershipEntityConfigs - entityConfigs.push(entityConfig) if entityConfig.fields.write? - - # remove the user from all entities types - async.map entityConfigs, ((entityConfig, innerCallback) -> - UserMembershipsHandler.removeUserFromEntities entityConfig, userId, innerCallback - ), callback - - removeUserFromEntities: (entityConfig, userId, callback = (error) ->) -> - removeOperation = "$pull": {} - removeOperation["$pull"][entityConfig.fields.write] = userId - EntityModels[entityConfig.modelName].updateMany {}, removeOperation, callback - - getEntitiesByUser: (entityConfig, userId, callback = (error, entities) ->) -> - query = Object.assign({}, entityConfig.baseQuery) - query[entityConfig.fields.access] = userId - EntityModels[entityConfig.modelName].find query, (error, entities = []) -> - return callback(error) if error? - async.mapSeries entities, - (entity, cb) -> entity.fetchV1Data(cb), - callback diff --git a/services/web/app/coffee/Features/V1/V1Api.coffee b/services/web/app/coffee/Features/V1/V1Api.coffee deleted file mode 100644 index 50ba940da5..0000000000 --- a/services/web/app/coffee/Features/V1/V1Api.coffee +++ /dev/null @@ -1,49 +0,0 @@ -request = require 'request' -settings = require 'settings-sharelatex' -Errors = require '../Errors/Errors' - -# TODO: check what happens when these settings aren't defined -DEFAULT_V1_PARAMS = { - baseUrl: settings?.apis?.v1?.url - auth: - user: settings?.apis?.v1?.user - pass: settings?.apis?.v1?.pass - json: true, - timeout: 30 * 1000 -} - -v1Request = request.defaults(DEFAULT_V1_PARAMS) - -DEFAULT_V1_OAUTH_PARAMS = { - baseUrl: settings?.apis?.v1?.url - json: true, - timeout: 30 * 1000 -} - -v1OauthRequest = request.defaults(DEFAULT_V1_OAUTH_PARAMS) - -module.exports = V1Api = - request: (options, callback) -> - return request(options) if !callback? - v1Request options, (error, response, body) -> - V1Api._responseHandler options, error, response, body, callback - - oauthRequest: (options, token, callback) -> - return callback(new Error "uri required") unless options.uri? - options.method = "GET" unless options.method? - options.auth = bearer: token - v1OauthRequest options, (error, response, body) -> - V1Api._responseHandler options, error, response, body, callback - - _responseHandler: (options, error, response, body, callback) -> - return callback(error, response, body) if error? - if 200 <= response.statusCode < 300 or response.statusCode in (options.expectedStatusCodes or []) - callback null, response, body - else if response.statusCode == 403 - error = new Errors.ForbiddenError("overleaf v1 returned forbidden") - error.statusCode = response.statusCode - callback error - else - error = new Error("overleaf v1 returned non-success code: #{response.statusCode} #{options.method} #{options.uri}") - error.statusCode = response.statusCode - callback error diff --git a/services/web/app/coffee/Features/V1/V1Handler.coffee b/services/web/app/coffee/Features/V1/V1Handler.coffee deleted file mode 100644 index f6dd20b268..0000000000 --- a/services/web/app/coffee/Features/V1/V1Handler.coffee +++ /dev/null @@ -1,60 +0,0 @@ -V1Api = require './V1Api' -Settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' - - -module.exports = V1Handler = - - authWithV1: (email, password, callback=(err, isValid, v1Profile)->) -> - V1Api.request { - method: 'POST', - url: '/api/v1/sharelatex/login', - json: {email, password}, - expectedStatusCodes: [403] - }, (err, response, body) -> - if err? - logger.err {email, err}, - "[V1Handler] error while talking to v1 login api" - return callback(err) - if response.statusCode in [200, 403] - isValid = body.valid - userProfile = body.user_profile - logger.log {email, isValid, v1UserId: body?.user_profile?.id}, - "[V1Handler] got response from v1 login api" - callback(null, isValid, userProfile) - else - err = new Error("Unexpected status from v1 login api: #{response.statusCode}") - callback(err) - - doPasswordReset: (v1_user_id, password, callback=(err, created)->) -> - logger.log({v1_user_id}, - "sending password reset request to v1 login api") - V1Api.request { - method: 'POST' - url: "/api/v1/sharelatex/reset_password" - json: { - user_id: v1_user_id, - password: password - } - expectedStatusCodes: [200] - }, (err, response, body) -> - if err? - logger.err {v1_user_id, err}, "error while talking to v1 password reset api" - return callback(err, false) - if response.statusCode in [200] - logger.log {v1_user_id, changed: true}, "got success response from v1 password reset api" - callback(null, true) - else - err = new Error("Unexpected status from v1 password reset api: #{response.statusCode}") - callback(err, false) - - getDocExported: (token, callback=(err, info)->) -> - # default to not exported - return callback(null, { - exported: false - exporting: false - }) unless Settings.apis?.v1? - - V1Api.request { url: "/api/v1/sharelatex/docs/#{token}/exported_to_v2" }, (err, response, body) -> - return callback err if err? - callback null, body diff --git a/services/web/app/coffee/infrastructure/CrawlerLogger.coffee b/services/web/app/coffee/infrastructure/CrawlerLogger.coffee deleted file mode 100644 index 4b3a2cd702..0000000000 --- a/services/web/app/coffee/infrastructure/CrawlerLogger.coffee +++ /dev/null @@ -1,11 +0,0 @@ -metrics = require('metrics-sharelatex') -module.exports = - log: (req)-> - if req.headers["user-agent"]? - userAgent = req.headers["user-agent"].toLowerCase() - if userAgent.indexOf("google") != -1 - metrics.inc "crawler.google" - else if userAgent.indexOf("facebook") != -1 - metrics.inc "crawler.facebook" - else if userAgent.indexOf("bing") != -1 - metrics.inc "crawler.bing" diff --git a/services/web/app/coffee/infrastructure/Csrf.coffee b/services/web/app/coffee/infrastructure/Csrf.coffee deleted file mode 100644 index 660e2d5a66..0000000000 --- a/services/web/app/coffee/infrastructure/Csrf.coffee +++ /dev/null @@ -1,56 +0,0 @@ -csurf = require('csurf') -csrf = csurf() - -# Wrapper for `csurf` middleware that provides a list of routes that can be excluded from csrf checks. -# -# Include with `Csrf = require('./Csrf')` -# -# Add the middleware to the router with: -# myRouter.csrf = new Csrf() -# myRouter.use webRouter.csrf.middleware -# When building routes, specify a route to exclude from csrf checks with: -# myRouter.csrf.disableDefaultCsrfProtection "/path" "METHOD" -# -# To validate the csrf token in a request to ensure that it's valid, you can use `validateRequest`, which takes a -# request object and calls a callback with either true or false. - -module.exports = class Csrf - constructor: -> - @excluded_routes = {} - - disableDefaultCsrfProtection: (route, method) -> - @excluded_routes[route] = {} unless @excluded_routes[route] - @excluded_routes[route][method] = 1 - - middleware: (req, res, next) => - # We want to call the middleware for all routes, even if excluded, because csurf sets up a csrfToken() method on - # the request, to get a new csrf token for any rendered forms. For excluded routes we'll then ignore a 'bad csrf - # token' error from csurf and continue on... - - # check whether the request method is excluded for the specified route - if @excluded_routes[req.path]?[req.method] == 1 - # ignore the error if it's due to a bad csrf token, and continue - csrf req, res, (err) => - if (err && err.code != 'EBADCSRFTOKEN') - next(err) - else - next() - else - csrf req, res, next - - @validateRequest: (req, cb = (valid)->) -> - # run a dummy csrf check to see if it returns an error - csrf req, null, (err) -> - cb(!err?) - - @validateToken: (token, session, cb = (valid)->) -> - return cb(false) unless token? - # run a dummy csrf check to see if it returns an error - # use this to simulate a csrf check regardless of req method, headers &c. - req = - body: - _csrf: token - headers: {} - method: 'POST' - session: session - Csrf.validateRequest(req, cb) diff --git a/services/web/app/coffee/infrastructure/Events.coffee b/services/web/app/coffee/infrastructure/Events.coffee deleted file mode 100644 index a7ff219b7a..0000000000 --- a/services/web/app/coffee/infrastructure/Events.coffee +++ /dev/null @@ -1,2 +0,0 @@ -events = require "events" -module.exports = new events.EventEmitter() \ No newline at end of file diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee deleted file mode 100644 index c0a91e7c91..0000000000 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ /dev/null @@ -1,378 +0,0 @@ -logger = require 'logger-sharelatex' -fs = require 'fs' -crypto = require 'crypto' -Settings = require('settings-sharelatex') -SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters') -querystring = require('querystring') -SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager") -AuthenticationController = require("../Features/Authentication/AuthenticationController") -_ = require("underscore") -async = require("async") -Modules = require "./Modules" -Url = require "url" -PackageVersions = require "./PackageVersions" -htmlEncoder = new require("node-html-encoder").Encoder("numerical") -hashedFiles = {} -Path = require 'path' -Features = require "./Features" -Modules = require "./Modules" -moment = require 'moment' - -jsPath = - if Settings.useMinifiedJs - "/minjs/" - else - "/js/" - -ace = PackageVersions.lib('ace') -pdfjs = PackageVersions.lib('pdfjs') -fineuploader = PackageVersions.lib('fineuploader') - -getFileContent = (filePath)-> - filePath = Path.join __dirname, "../../../", "public#{filePath}" - exists = fs.existsSync filePath - if exists - content = fs.readFileSync filePath, "UTF-8" - return content - else - logger.log filePath:filePath, "file does not exist for hashing" - return "" - -pathList = [ - "#{jsPath}libs/require.js" - "#{jsPath}ide.js" - "#{jsPath}main.js" - "#{jsPath}libraries.js" - "/stylesheets/style.css" - "/stylesheets/light-style.css" - "/stylesheets/ieee-style.css" - "/stylesheets/sl-style.css" -].concat(Modules.moduleAssetFiles(jsPath)) - -if !Settings.useMinifiedJs - logger.log "not using minified JS, not hashing static files" -else - logger.log "Generating file hashes..." - for path in pathList - content = getFileContent(path) - hash = crypto.createHash("md5").update(content).digest("hex") - - splitPath = path.split("/") - filenameSplit = splitPath.pop().split(".") - filenameSplit.splice(filenameSplit.length-1, 0, hash) - splitPath.push(filenameSplit.join(".")) - - hashPath = splitPath.join("/") - hashedFiles[path] = hashPath - - fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}" - fs.writeFileSync(fsHashPath, content) - - - logger.log "Finished hashing static content" - -cdnAvailable = Settings.cdn?.web?.host? -darkCdnAvailable = Settings.cdn?.web?.darkHost? - -module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> - webRouter.use (req, res, next)-> - res.locals.session = req.session - next() - - addSetContentDisposition = (req, res, next) -> - res.setContentDisposition = (type, opts) -> - directives = for k, v of opts - "#{k}=\"#{encodeURIComponent(v)}\"" - contentDispositionValue = "#{type}; #{directives.join('; ')}" - res.setHeader( - 'Content-Disposition', - contentDispositionValue - ) - next() - webRouter.use addSetContentDisposition - privateApiRouter.use addSetContentDisposition - publicApiRouter.use addSetContentDisposition - - webRouter.use (req, res, next)-> - req.externalAuthenticationSystemUsed = Features.externalAuthenticationSystemUsed - res.locals.externalAuthenticationSystemUsed = Features.externalAuthenticationSystemUsed - req.hasFeature = res.locals.hasFeature = Features.hasFeature - res.locals.userIsFromOLv1 = (user) -> - user.overleaf?.id? - res.locals.userIsFromSL = (user) -> - !user.overleaf?.id? - next() - - webRouter.use (req, res, next)-> - - cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked - user_id = AuthenticationController.getLoggedInUserId(req) - - if cdnBlocked and !req.session.cdnBlocked? - logger.log user_id:user_id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets" - req.session.cdnBlocked = true - - isDark = req.headers?.host?.slice(0,7)?.toLowerCase().indexOf("dark") != -1 - isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke" - isLive = !isDark and !isSmoke - - if cdnAvailable and isLive and !cdnBlocked - 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.lib = PackageVersions.lib - - res.locals.moment = moment - - res.locals.buildJsPath = (jsFile, opts = {})-> - path = Path.join(jsPath, jsFile) - - if opts.hashedPath && hashedFiles[path]? - path = hashedFiles[path] - - if !opts.qs? - opts.qs = {} - - if opts.cdn != false - path = Url.resolve(staticFilesBase, path) - - qs = querystring.stringify(opts.qs) - - if opts.removeExtension == true - path = path.slice(0,-3) - - if qs? and qs.length > 0 - path = path + "?" + qs - return path - - res.locals.buildWebpackPath = (jsFile, opts = {}) -> - if Settings.webpack? and !Settings.useMinifiedJs - path = Path.join(jsPath, jsFile) - if opts.removeExtension == true - path = path.slice(0,-3) - return "#{Settings.webpack.url}/public#{path}" - else - return res.locals.buildJsPath(jsFile, opts) - - - IEEE_BRAND_ID = 15 - res.locals.isIEEE = (brandVariation) -> - brandVariation?.brand_id == IEEE_BRAND_ID - - _buildCssFileName = (themeModifier) -> - return "/" + Settings.brandPrefix + (if themeModifier then themeModifier else "") + "style.css" - - res.locals.getCssThemeModifier = (userSettings, brandVariation) -> - # Themes only exist in OL v2 - if Settings.overleaf? - # The IEEE theme takes precedence over the user personal setting, i.e. a user with - # a theme setting of "light" will still get the IEE theme in IEEE branded projects. - if res.locals.isIEEE(brandVariation) - themeModifier = "ieee-" - else if userSettings?.overallTheme? - themeModifier = userSettings.overallTheme - return themeModifier - - res.locals.buildCssPath = (themeModifier, buildOpts) -> - cssFileName = _buildCssFileName themeModifier - path = Path.join("/stylesheets/", cssFileName) - if buildOpts?.hashedPath && hashedFiles[path]? - hashedPath = hashedFiles[path] - return Url.resolve(staticFilesBase, hashedPath) - return Url.resolve(staticFilesBase, path) - - res.locals.buildImgPath = (imgFile)-> - path = Path.join("/img/", imgFile) - return Url.resolve(staticFilesBase, path) - - res.locals.mathJaxPath = res.locals.buildJsPath( - 'libs/mathjax/MathJax.js', - {cdn:false, qs:{config:'TeX-AMS_HTML,Safe'}} - ) - - next() - - webRouter.use (req, res, next)-> - res.locals.settings = Settings - next() - - webRouter.use (req, res, next)-> - res.locals.translate = (key, vars = {}, htmlEncode = false) -> - vars.appName = Settings.appName - str = req.i18n.translate(key, vars) - if htmlEncode then htmlEncoder.htmlEncode(str) else str - # Don't include the query string parameters, otherwise Google - # treats ?nocdn=true as the canonical version - res.locals.currentUrl = Url.parse(req.originalUrl).pathname - res.locals.capitalize = (string) -> - return "" if string.length == 0 - return string.charAt(0).toUpperCase() + string.slice(1) - next() - - webRouter.use (req, res, next)-> - res.locals.getSiteHost = -> - Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2) - next() - - webRouter.use (req, res, next) -> - res.locals.getUserEmail = -> - user = AuthenticationController.getSessionUser(req) - email = user?.email or "" - return email - next() - - webRouter.use (req, res, next) -> - res.locals.StringHelper = require('../Features/Helpers/StringHelper') - next() - - webRouter.use (req, res, next)-> - res.locals.formatProjectPublicAccessLevel = (privilegeLevel)-> - formatedPrivileges = private:"Private", readOnly:"Public: Read Only", readAndWrite:"Public: Read and Write" - return formatedPrivileges[privilegeLevel] || "Private" - next() - - webRouter.use (req, res, next)-> - res.locals.buildReferalUrl = (referal_medium) -> - url = Settings.siteUrl - currentUser = AuthenticationController.getSessionUser(req) - if currentUser? and currentUser?.referal_id? - url+="?r=#{currentUser.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus - return url - res.locals.getReferalId = -> - currentUser = AuthenticationController.getSessionUser(req) - if currentUser? and currentUser?.referal_id? - return currentUser.referal_id - res.locals.getReferalTagLine = -> - tagLines = [ - "Roar!" - "Shout about us!" - "Please recommend us" - "Tell the world!" - "Thanks for using ShareLaTeX" - ] - return tagLines[Math.floor(Math.random()*tagLines.length)] - res.locals.getRedirAsQueryString = -> - if req.query.redir? - return "?#{querystring.stringify({redir:req.query.redir})}" - return "" - - res.locals.getLoggedInUserId = -> - return AuthenticationController.getLoggedInUserId(req) - res.locals.isUserLoggedIn = -> - return AuthenticationController.isUserLoggedIn(req) - res.locals.getSessionUser = -> - return AuthenticationController.getSessionUser(req) - - next() - - webRouter.use (req, res, next) -> - res.locals.csrfToken = req?.csrfToken() - next() - - webRouter.use (req, res, next) -> - res.locals.getReqQueryParam = (field)-> - return req.query?[field] - next() - - webRouter.use (req, res, next)-> - res.locals.formatPrice = SubscriptionFormatters.formatPrice - next() - - webRouter.use (req, res, next)-> - currentUser = AuthenticationController.getSessionUser(req) - if currentUser? - res.locals.user = - email: currentUser.email - first_name: currentUser.first_name - last_name: currentUser.last_name - if req.session.justRegistered - res.locals.justRegistered = true - delete req.session.justRegistered - if req.session.justLoggedIn - res.locals.justLoggedIn = true - delete req.session.justLoggedIn - res.locals.gaToken = Settings.analytics?.ga?.token - res.locals.tenderUrl = Settings.tenderUrl - res.locals.sentrySrc = Settings.sentry?.src - res.locals.sentryPublicDSN = Settings.sentry?.publicDSN - next() - - webRouter.use (req, res, next) -> - if req.query? and req.query.scribtex_path? - res.locals.lookingForScribtex = true - res.locals.scribtexPath = req.query.scribtex_path - next() - - webRouter.use (req, res, next) -> - # Clone the nav settings so they can be modified for each request - res.locals.nav = {} - for key, value of Settings.nav - res.locals.nav[key] = _.clone(Settings.nav[key]) - res.locals.templates = Settings.templateLinks - if res.locals.nav.header - console.error {}, "The `nav.header` setting is no longer supported, use `nav.header_extras` instead" - next() - - webRouter.use (req, res, next) -> - SystemMessageManager.getMessages (error, messages = []) -> - res.locals.systemMessages = messages - next() - - webRouter.use (req, res, next)-> - res.locals.query = req.query - next() - - webRouter.use (req, res, next)-> - subdomain = _.find Settings.i18n.subdomainLang, (subdomain)-> - subdomain.lngCode == req.showUserOtherLng and !subdomain.hide - res.locals.recomendSubdomain = subdomain - res.locals.currentLngCode = req.lng - next() - - webRouter.use (req, res, next) -> - if Settings.reloadModuleViewsOnEachRequest - Modules.loadViewIncludes() - res.locals.moduleIncludes = Modules.moduleIncludes - res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable - next() - - webRouter.use (req, res, next) -> - isSl = (Settings.brandPrefix == 'sl-') - res.locals.uiConfig = - defaultResizerSizeOpen : if isSl then 24 else 7 - defaultResizerSizeClosed : if isSl then 24 else 7 - eastResizerCursor : if isSl then null else "ew-resize" - westResizerCursor : if isSl then null else "ew-resize" - chatResizerSizeOpen : if isSl then 12 else 7 - chatResizerSizeClosed : 0 - chatMessageBorderSaturation: if isSl then "70%" else "85%" - chatMessageBorderLightness : if isSl then "70%" else "40%" - chatMessageBgSaturation : if isSl then "60%" else "85%" - chatMessageBgLightness : if isSl then "97%" else "40%" - defaultFontFamily : if isSl then 'monaco' else 'lucida' - defaultLineHeight : if isSl then 'compact' else 'normal' - renderAnnouncements : isSl - next() - - webRouter.use (req, res, next) -> - #TODO - if Settings.overleaf? - res.locals.overallThemes = [ - { name: "Default", val: "", path: res.locals.buildCssPath(null, { hashedPath: true }) } - { name: "Light", val: "light-", path: res.locals.buildCssPath("light-", { hashedPath: true }) } - ] - next() - - webRouter.use (req, res, next) -> - res.locals.ExposedSettings = - isOverleaf: Settings.overleaf? - appName: Settings.appName - siteUrl: Settings.siteUrl - recaptchaSiteKeyV3: Settings.recaptcha?.siteKeyV3 - recaptchaDisabled: Settings.recaptcha?.disabled - next() diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee deleted file mode 100644 index 7e53cbd29b..0000000000 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ /dev/null @@ -1,34 +0,0 @@ -Settings = require 'settings-sharelatex' - -module.exports = Features = - externalAuthenticationSystemUsed: -> - Settings.ldap? or Settings.saml? or Settings.overleaf?.oauth? - - hasFeature: (feature) -> - switch feature - when 'homepage' - return Settings.enableHomepage - when 'registration' - return not Features.externalAuthenticationSystemUsed() or Settings.overleaf? - when 'github-sync' - return Settings.enableGithubSync - when 'git-bridge' - return Settings.enableGitBridge - when 'v1-return-message' - return Settings.accountMerge? and Settings.overleaf? and !Settings.forceImportToV2 - when 'custom-togglers' - return Settings.overleaf? - when 'oauth' - return Settings.oauth? - when 'publish-templates' - return true - when 'view-templates' - return !Settings.overleaf? - when 'affiliations' - return Settings?.apis?.v1?.url? - when 'redirect-sl' - return Settings.redirectToV2? - when 'force-import-to-v2' - return Settings.forceImportToV2 - else - throw new Error("unknown feature: #{feature}") diff --git a/services/web/app/coffee/infrastructure/FileWriter.coffee b/services/web/app/coffee/infrastructure/FileWriter.coffee deleted file mode 100644 index e8b9de8fea..0000000000 --- a/services/web/app/coffee/infrastructure/FileWriter.coffee +++ /dev/null @@ -1,60 +0,0 @@ -fs = require 'fs' -logger = require 'logger-sharelatex' -uuid = require 'uuid' -_ = require 'underscore' -Settings = require 'settings-sharelatex' -request = require 'request' - -module.exports = FileWriter = - - ensureDumpFolderExists: (callback=(error)->) -> - fs.mkdir Settings.path.dumpFolder, (error) -> - if error? and error.code != 'EEXIST' - # Ignore error about already existing - return callback(error) - callback(null) - - writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) -> - FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback) - - writeContentToDisk: (identifier, content, callback = (error, fsPath)->) -> - callback = _.once(callback) - fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" - FileWriter.ensureDumpFolderExists (error) -> - return callback(error) if error? - fs.writeFile fsPath, content, (error) -> - return callback(error) if error? - callback(null, fsPath) - - writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) -> - callback = _.once(callback) - fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" - - stream.pause() - FileWriter.ensureDumpFolderExists (error) -> - return callback(error) if error? - stream.resume() - - writeStream = fs.createWriteStream(fsPath) - stream.pipe(writeStream) - - stream.on 'error', (err)-> - logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with incoming stream" - callback(err) - writeStream.on 'error', (err)-> - logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with writing to disk" - callback(err) - writeStream.on "finish", -> - logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished" - callback null, fsPath - - writeUrlToDisk: (identifier, url, callback = (error, fsPath) ->) -> - callback = _.once(callback) - stream = request.get(url) - stream.on 'response', (response) -> - if 200 <= response.statusCode < 300 - FileWriter.writeStreamToDisk identifier, stream, callback - else - err = new Error("bad response from url: #{response.statusCode}") - logger.err {err, identifier, url}, err.message - callback(err) diff --git a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee deleted file mode 100644 index e2b36e94ab..0000000000 --- a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee +++ /dev/null @@ -1,52 +0,0 @@ -request = require("request") -settings = require("settings-sharelatex") -_ = require("underscore") -logger = require("logger-sharelatex") -URL = require("url") - -currencyMappings = { - "GB":"GBP" - "US":"USD" - "CH":"CHF" - "NZ":"NZD" - "AU":"AUD" - "DK":"DKK" - "NO":"NOK" - "CA":"CAD" - "SE":"SEK" -} - -# Countries which would likely prefer Euro's -EuroCountries = ["AT", "BE", "BG", "HR", "CY", "CZ", -"EE", "FI", "FR", "DE", "EL", "HU", "IE", -"IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", -"RO", "SK", "SI", "ES"] - -_.each EuroCountries, (country)-> currencyMappings[country] = "EUR" - -module.exports = GeoIpLookup = - - getDetails : (ip, callback)-> - if !ip? - e = new Error("no ip passed") - return callback(e) - ip = ip.trim().split(" ")[0] - opts = - url: URL.resolve(settings.apis.geoIpLookup.url,ip) - timeout: 1000 - json:true - logger.log ip:ip, opts:opts, "getting geo ip details" - request.get opts, (err, res, ipDetails)-> - if err? - logger.err err:err, ip:ip, "error getting ip details" - callback(err, ipDetails) - - getCurrencyCode : (ip, callback)-> - GeoIpLookup.getDetails ip, (err, ipDetails)-> - if err? or !ipDetails? - logger.err err:err, ip:ip, "problem getting currencyCode for ip, defaulting to USD" - return callback(null, "USD") - countryCode = ipDetails?.country_code?.toUpperCase() - currencyCode = currencyMappings[countryCode] || "USD" - logger.log ip:ip, currencyCode:currencyCode, ipDetails:ipDetails, "got currencyCode for ip" - callback(err, currencyCode, countryCode) diff --git a/services/web/app/coffee/infrastructure/Keys.coffee b/services/web/app/coffee/infrastructure/Keys.coffee deleted file mode 100644 index 2966f35aa3..0000000000 --- a/services/web/app/coffee/infrastructure/Keys.coffee +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = - - queue: - web_to_tpds_http_requests: "web_to_tpds_http_requests" - tpds_to_web_http_requests: "tpds_to_web_http_requests" diff --git a/services/web/app/coffee/infrastructure/LockManager.coffee b/services/web/app/coffee/infrastructure/LockManager.coffee deleted file mode 100644 index 14be0dbb9e..0000000000 --- a/services/web/app/coffee/infrastructure/LockManager.coffee +++ /dev/null @@ -1,128 +0,0 @@ -metrics = require('metrics-sharelatex') -Settings = require('settings-sharelatex') -RedisWrapper = require("./RedisWrapper") -rclient = RedisWrapper.client("lock") -logger = require "logger-sharelatex" -os = require "os" -crypto = require "crypto" -async = require "async" - -HOST = os.hostname() -PID = process.pid -RND = crypto.randomBytes(4).toString('hex') -COUNT = 0 - -LOCK_QUEUES = new Map() # queue lock requests for each name/id so they get the lock on a first-come first-served basis - -module.exports = LockManager = - LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock - MAX_TEST_INTERVAL: 1000 # back off to 1s between each test of the lock - MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock - REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis - SLOW_EXECUTION_THRESHOLD: 5000 # 5s, if execution takes longer than this then log - - # Use a signed lock value as described in - # http://redis.io/topics/distlock#correct-implementation-with-a-single-instance - # to prevent accidental unlocking by multiple processes - randomLock : () -> - time = Date.now() - return "locked:host=#{HOST}:pid=#{PID}:random=#{RND}:time=#{time}:count=#{COUNT++}" - - unlockScript: 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end' - - runWithLock: (namespace, id, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) -> - # This error is defined here so we get a useful stacktrace - slowExecutionError = new Error "slow execution during lock" - - timer = new metrics.Timer("lock.#{namespace}") - key = "lock:web:#{namespace}:#{id}" - LockManager._getLock key, namespace, (error, lockValue) -> - return callback(error) if error? - - # The lock can expire in redis but the process carry on. This setTimout call - # is designed to log if this happens. - countIfExceededLockTimeout = () -> - metrics.inc "lock.#{namespace}.exceeded_lock_timeout" - logger.log "exceeded lock timeout", { namespace, id, slowExecutionError } - exceededLockTimeout = setTimeout countIfExceededLockTimeout, LockManager.REDIS_LOCK_EXPIRY * 1000 - - runner (error1, values...) -> - LockManager._releaseLock key, lockValue, (error2) -> - clearTimeout exceededLockTimeout - - timeTaken = new Date - timer.start - if timeTaken > LockManager.SLOW_EXECUTION_THRESHOLD - logger.log "slow execution during lock", { namespace, id, timeTaken, slowExecutionError } - - timer.done() - error = error1 or error2 - return callback(error) if error? - callback null, values... - - _tryLock : (key, namespace, callback = (err, isFree, lockValue)->)-> - lockValue = LockManager.randomLock() - rclient.set key, lockValue, "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)-> - return callback(err) if err? - if gotLock == "OK" - metrics.inc "lock.#{namespace}.try.success" - callback err, true, lockValue - else - metrics.inc "lock.#{namespace}.try.failed" - logger.log key: key, redis_response: gotLock, "lock is locked" - callback err, false - - # it's sufficient to serialize within a process because that is where the parallel operations occur - _getLock: (key, namespace, callback = (error, lockValue) ->) -> - # this is what we need to do for each lock we want to request - task = (next) -> - LockManager._getLockByPolling key, namespace, (error, lockValue) -> - # tell the queue to start trying to get the next lock (if any) - next() - # we have got a lock result, so we can continue with our own execution - callback(error, lockValue) - # create a queue for this key if needed - queueName = "#{key}:#{namespace}" - queue = LOCK_QUEUES.get queueName - if !queue? - handler = (fn, cb) -> - fn(cb) # execute any function as our task - # set up a new queue for this key - queue = async.queue handler, 1 - queue.push task - # remove the queue object when queue is empty - queue.drain = () -> - LOCK_QUEUES.delete queueName - # store the queue in our global map - LOCK_QUEUES.set queueName, queue - else - # queue the request to get the lock - queue.push task - - _getLockByPolling: (key, namespace, callback = (error, lockValue) ->) -> - startTime = Date.now() - testInterval = LockManager.LOCK_TEST_INTERVAL - attempts = 0 - do attempt = () -> - if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME - metrics.inc "lock.#{namespace}.get.failed" - return callback(new Error("Timeout")) - - attempts += 1 - LockManager._tryLock key, namespace, (error, gotLock, lockValue) -> - return callback(error) if error? - if gotLock - metrics.gauge "lock.#{namespace}.get.success.tries", attempts - callback(null, lockValue) - else - setTimeout attempt, testInterval - - _releaseLock: (key, lockValue, callback)-> - rclient.eval LockManager.unlockScript, 1, key, lockValue, (err, result) -> - if err? - return callback(err) - else if result? and result isnt 1 # successful unlock should release exactly one key - logger.error {key:key, lockValue:lockValue, redis_err:err, redis_result:result}, "unlocking error" - metrics.inc "unlock-error" - return callback(new Error("tried to release timed out lock")) - else - callback(null,result) diff --git a/services/web/app/coffee/infrastructure/LoggerSerializers.coffee b/services/web/app/coffee/infrastructure/LoggerSerializers.coffee deleted file mode 100644 index 33e0078cc8..0000000000 --- a/services/web/app/coffee/infrastructure/LoggerSerializers.coffee +++ /dev/null @@ -1,40 +0,0 @@ -module.exports = - user: (user) -> - if !user? - return null - if !user._id? - user = {_id : user} - return { - id: user._id - email: user.email - first_name: user.name - last_name: user.name - } - - project: (project) -> - if !project? - return null - if !project._id? - project = {_id: project} - return { - id: project._id - name: project.name - } - - docs: (docs) -> - if !docs?.map? - return - docs.map (doc) -> - { - path: doc.path - id: doc.doc - } - - files: (files) -> - if !files?.map? - return - files.map (file) -> - { - path: file.path - id: file.file - } diff --git a/services/web/app/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee deleted file mode 100644 index a7944e54dc..0000000000 --- a/services/web/app/coffee/infrastructure/Modules.coffee +++ /dev/null @@ -1,81 +0,0 @@ -fs = require "fs" -Path = require "path" -pug = require "pug" -async = require "async" - -MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules") - -module.exports = Modules = - modules: [] - loadModules: () -> - for moduleName in fs.readdirSync(MODULE_BASE_PATH) - if fs.existsSync(Path.join(MODULE_BASE_PATH, moduleName, "index.js")) - loadedModule = require(Path.join(MODULE_BASE_PATH, moduleName, "index")) - loadedModule.name = moduleName - @modules.push loadedModule - Modules.attachHooks() - - applyRouter: (webRouter, privateApiRouter, publicApiRouter) -> - for module in @modules - module.router?.apply?(webRouter, privateApiRouter, publicApiRouter) - - applyNonCsrfRouter: (webRouter, privateApiRouter, publicApiRouter) -> - for module in @modules - module.nonCsrfRouter?.apply(webRouter, privateApiRouter, publicApiRouter) - module.router?.applyNonCsrfRouter?(webRouter, privateApiRouter, publicApiRouter) - - viewIncludes: {} - loadViewIncludes: (app) -> - @viewIncludes = {} - for module in @modules - for view, partial of module.viewIncludes or {} - @viewIncludes[view] ||= [] - filePath = Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug") - @viewIncludes[view].push pug.compileFile(filePath, doctype: "html") - - moduleIncludes: (view, locals) -> - compiledPartials = Modules.viewIncludes[view] or [] - html = "" - for compiledPartial in compiledPartials - d = new Date() - html += compiledPartial(locals) - return html - - moduleIncludesAvailable: (view) -> - return (Modules.viewIncludes[view] or []).length > 0 - - moduleAssetFiles: (pathPrefix) -> - assetFiles = [] - for module in @modules - for assetFile in module.assetFiles or [] - assetFiles.push "#{pathPrefix}#{assetFile}" - return assetFiles - - linkedFileAgentsIncludes: () -> - agents = {} - for module in @modules - for name, agentFunction of module.linkedFileAgents - agents[name] = agentFunction() - return agents - - attachHooks: () -> - for module in @modules - if module.hooks? - for hook, method of module.hooks - Modules.hooks.attach hook, method - - hooks: - _hooks: {} - attach: (name, method) -> - @_hooks[name] ?= [] - @_hooks[name].push method - - fire: (name, args..., callback) -> - methods = @_hooks[name] or [] - call_methods = methods.map (method) -> - return (cb) -> method(args..., cb) - async.series call_methods, (error, results) -> - return callback(error) if error? - return callback null, results - -Modules.loadModules() diff --git a/services/web/app/coffee/infrastructure/Mongoose.coffee b/services/web/app/coffee/infrastructure/Mongoose.coffee deleted file mode 100644 index b1af2ac664..0000000000 --- a/services/web/app/coffee/infrastructure/Mongoose.coffee +++ /dev/null @@ -1,23 +0,0 @@ -mongoose = require('mongoose') -Settings = require 'settings-sharelatex' -logger = require('logger-sharelatex') - -mongoose.connect(Settings.mongo.url, { - server: {poolSize: 10}, - config: {autoIndex: false} -}) - -mongoose.connection.on 'connected', () -> - logger.log {url:Settings.mongo.url}, 'mongoose default connection open' - -mongoose.connection.on 'error', (err) -> - logger.err err:err, 'mongoose error on default connection'; - -mongoose.connection.on 'disconnected', () -> - logger.log 'mongoose default connection disconnected' - -if process.env.MONGOOSE_DEBUG - mongoose.set 'debug', (collectionName, method, query, doc) -> - logger.debug 'mongoose debug', collectionName: collectionName, method: method, query: query, doc: doc - -module.exports = mongoose diff --git a/services/web/app/coffee/infrastructure/PackageVersions.coffee b/services/web/app/coffee/infrastructure/PackageVersions.coffee deleted file mode 100644 index d1c81b301d..0000000000 --- a/services/web/app/coffee/infrastructure/PackageVersions.coffee +++ /dev/null @@ -1,16 +0,0 @@ -version = { - "pdfjs": "2.0.943" - "moment": "2.9.0" - "ace": "1.4.4" # Upgrade instructions: https://github.com/overleaf/write_latex/wiki/Upgrading-Ace - "fineuploader": "5.15.4" -} - -module.exports = { - version: version - - lib: (name) -> - if version[name]? - return "#{name}-#{version[name]}" - else - return "#{name}" -} diff --git a/services/web/app/coffee/infrastructure/ProxyManager.coffee b/services/web/app/coffee/infrastructure/ProxyManager.coffee deleted file mode 100644 index 3e7a037054..0000000000 --- a/services/web/app/coffee/infrastructure/ProxyManager.coffee +++ /dev/null @@ -1,46 +0,0 @@ -settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' -request = require 'request' -URL = require 'url' - -module.exports = ProxyManager = - apply: (publicApiRouter) -> - for proxyUrl, target of settings.proxyUrls - do (target) -> - method = target.options?.method || 'get' - publicApiRouter[method] proxyUrl, ProxyManager.createProxy(target) - - createProxy: (target) -> - (req, res, next) -> - targetUrl = makeTargetUrl(target, req) - logger.log targetUrl: targetUrl, reqUrl: req.url, "proxying url" - - options = - url: targetUrl - options.headers = { Cookie: req.headers.cookie } if req.headers?.cookie - Object.assign(options, target.options) if target?.options? - options.form = req.body if options.method in ['post', 'put'] - upstream = request(options) - upstream.on "error", (error) -> - logger.error err: error, "error in ProxyManager" - - # TODO: better handling of status code - # see https://github.com/overleaf/write_latex/wiki/Streams-and-pipes-in-Node.js - upstream.pipe(res) - -# make a URL from a proxy target. -# if the query is specified, set/replace the target's query with the given query -makeTargetUrl = (target, req) -> - targetUrl = URL.parse(parseSettingUrl(target, req)) - if req.query? and Object.keys(req.query).length > 0 - targetUrl.query = req.query - targetUrl.search = null # clear `search` as it takes precedence over `query` - targetUrl.format() - -parseSettingUrl = (target, { params }) -> - return target if typeof target is 'string' - if typeof target.path is 'function' - path = target.path(params) - else - path = target.path - "#{target.baseUrl}#{path or ''}" diff --git a/services/web/app/coffee/infrastructure/RandomLogging.coffee b/services/web/app/coffee/infrastructure/RandomLogging.coffee deleted file mode 100644 index 6657fb3aae..0000000000 --- a/services/web/app/coffee/infrastructure/RandomLogging.coffee +++ /dev/null @@ -1,6 +0,0 @@ -_ = require('underscore') -metrics = require('metrics-sharelatex') - -do trackOpenSockets = -> - metrics.gauge("http.open-sockets", _.size(require('http').globalAgent.sockets.length), 0.5) - setTimeout(trackOpenSockets, 1000) diff --git a/services/web/app/coffee/infrastructure/RateLimiter.coffee b/services/web/app/coffee/infrastructure/RateLimiter.coffee deleted file mode 100644 index 519bf5fc87..0000000000 --- a/services/web/app/coffee/infrastructure/RateLimiter.coffee +++ /dev/null @@ -1,29 +0,0 @@ -settings = require("settings-sharelatex") -Metrics = require('metrics-sharelatex') -RedisWrapper = require('./RedisWrapper') -rclient = RedisWrapper.client('ratelimiter') -RollingRateLimiter = require('rolling-rate-limiter') - - -module.exports = RateLimiter = - - addCount: (opts, callback = (err, shouldProcess)->)-> - namespace = "RateLimit:#{opts.endpointName}:" - k = "{#{opts.subjectName}}" - limiter = RollingRateLimiter({ - redis: rclient, - namespace: namespace, - interval: opts.timeInterval * 1000, - maxInInterval: opts.throttle - }) - limiter k, (err, timeLeft, actionsLeft) -> - if err? - return callback(err) - allowed = timeLeft == 0 - Metrics.inc "rate-limit-hit.#{opts.endpointName}", 1, {path: opts.endpointName} unless allowed - callback(null, allowed) - - clearRateLimit: (endpointName, subject, callback) -> - # same as the key which will be built by RollingRateLimiter (namespace+k) - keyName = "RateLimit:#{endpointName}:{#{subject}}" - rclient.del keyName, callback diff --git a/services/web/app/coffee/infrastructure/RedirectManager.coffee b/services/web/app/coffee/infrastructure/RedirectManager.coffee deleted file mode 100644 index d2a9f4b5cc..0000000000 --- a/services/web/app/coffee/infrastructure/RedirectManager.coffee +++ /dev/null @@ -1,44 +0,0 @@ -settings = require("settings-sharelatex") -logger = require("logger-sharelatex") -URL = require('url') -querystring = require('querystring') - -module.exports = RedirectManager = - apply: (webRouter) -> - for redirectUrl, target of settings.redirects - for method in (target.methods or ['get']) - webRouter[method] redirectUrl, RedirectManager.createRedirect(target) - - createRedirect: (target) -> - (req, res, next) -> - return next() if req.headers?['x-skip-redirects']? - code = 302 - if typeof target is 'string' - url = target - else - if req.method != "GET" - code = 307 - - if typeof target.url == "function" - url = target.url(req.params) - if !url - return next() - else - url = target.url - - # Special handling for redirecting to v1, to ensure that query params - # are encoded - if target.authWithV1 - url = "/sign_in_to_v1?" + querystring.stringify(return_to: url + getQueryString(req)) - return res.redirect code, url - - if target.baseUrl? - url = "#{target.baseUrl}#{url}" - res.redirect code, url + getQueryString(req) - -# Naively get the query params string. Stringifying the req.query object may -# have differences between Express and Rails, so safer to just pass the raw -# string -getQueryString = (req) -> - {search} = URL.parse(req.url) - if search then search else "" diff --git a/services/web/app/coffee/infrastructure/RedisWrapper.coffee b/services/web/app/coffee/infrastructure/RedisWrapper.coffee deleted file mode 100644 index 523785427e..0000000000 --- a/services/web/app/coffee/infrastructure/RedisWrapper.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Settings = require 'settings-sharelatex' -redis = require 'redis-sharelatex' - -# A per-feature interface to Redis, -# looks up the feature in `settings.redis` -# and returns an appropriate client. -# Necessary because we don't want to migrate web over -# to redis-cluster all at once. -module.exports = Redis = - # feature = 'websessions' | 'ratelimiter' | ... - client: (feature) -> - redisFeatureSettings = Settings.redis[feature] or Settings.redis.web - rclient = redis.createClient(redisFeatureSettings) - return rclient diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee deleted file mode 100644 index 913020cf83..0000000000 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ /dev/null @@ -1,208 +0,0 @@ -Path = require "path" -express = require('express') -Settings = require('settings-sharelatex') -logger = require 'logger-sharelatex' -metrics = require('metrics-sharelatex') -crawlerLogger = require('./CrawlerLogger') -expressLocals = require('./ExpressLocals') -Router = require('../router') -helmet = require "helmet" -UserSessionsRedis = require('../Features/User/UserSessionsRedis') -Csrf = require('./Csrf') - -sessionsRedisClient = UserSessionsRedis.client() - -session = require("express-session") -RedisStore = require('connect-redis')(session) -bodyParser = require('body-parser') -methodOverride = require('method-override') -cookieParser = require('cookie-parser') -bearerToken = require('express-bearer-token') - -# Init the session store -sessionStore = new RedisStore(client:sessionsRedisClient) - -passport = require('passport') -LocalStrategy = require('passport-local').Strategy - -Mongoose = require("./Mongoose") - -oneDayInMilliseconds = 86400000 -ReferalConnect = require('../Features/Referal/ReferalConnect') -RedirectManager = require("./RedirectManager") -ProxyManager = require("./ProxyManager") -translations = require("translations-sharelatex").setup(Settings.i18n) -Modules = require "./Modules" - -ErrorController = require "../Features/Errors/ErrorController" -UserSessionsManager = require "../Features/User/UserSessionsManager" -AuthenticationController = require "../Features/Authentication/AuthenticationController" - - -metrics.event_loop?.monitor(logger) - -if Settings.cacheStaticAssets - staticCacheAge = (oneDayInMilliseconds * 365) -else - staticCacheAge = 0 - -app = express() - -webRouter = express.Router() -privateApiRouter = express.Router() -publicApiRouter = express.Router() - -if Settings.behindProxy - app.enable('trust proxy') - -webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) -app.set 'views', __dirname + '/../../views' -app.set 'view engine', 'pug' -Modules.loadViewIncludes app - - - -app.use bodyParser.urlencoded({ extended: true, limit: "2mb"}) -# Make sure we can process twice the max doc length, to allow for -# - the doc content -# - text ranges spanning the whole doc -# Also allow some overhead for JSON encoding -app.use bodyParser.json({limit: 2 * Settings.max_doc_length + 64 * 1024}) # 64kb overhead -app.use methodOverride() -app.use bearerToken() - -app.use metrics.http.monitor(logger) -RedirectManager.apply(webRouter) -ProxyManager.apply(publicApiRouter) - - -webRouter.use cookieParser(Settings.security.sessionSecret) -webRouter.use session - resave: false - saveUninitialized:false - secret:Settings.security.sessionSecret - proxy: Settings.behindProxy - cookie: - domain: Settings.cookieDomain - maxAge: Settings.cookieSessionLength - secure: Settings.secureCookie - store: sessionStore - key: Settings.cookieName - rolling: true - -# passport -webRouter.use passport.initialize() -webRouter.use passport.session() - -passport.use(new LocalStrategy( - { - passReqToCallback: true, - usernameField: 'email', - passwordField: 'password' - }, - AuthenticationController.doPassportLogin -)) -passport.serializeUser(AuthenticationController.serializeUser) -passport.deserializeUser(AuthenticationController.deserializeUser) - -Modules.hooks.fire 'passportSetup', passport, (err) -> - if err? - logger.err {err}, "error setting up passport in modules" - -Modules.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) - -webRouter.csrf = new Csrf() -webRouter.use webRouter.csrf.middleware -webRouter.use translations.expressMiddlewear -webRouter.use translations.setLangBasedOnDomainMiddlewear - -# Measure expiry from last request, not last login -webRouter.use (req, res, next) -> - req.session.touch() - if AuthenticationController.isUserLoggedIn(req) - UserSessionsManager.touch(AuthenticationController.getSessionUser(req), (err)->) - next() - -webRouter.use ReferalConnect.use -expressLocals(app, webRouter, privateApiRouter, publicApiRouter) - -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() - -webRouter.use (req, res, next) -> - if Settings.siteIsOpen - next() - else - res.status(503) - res.render("general/closed", {title:"maintenance"}) - -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"}) - -# add security headers using Helmet -webRouter.use (req, res, next) -> - isLoggedIn = AuthenticationController.isUserLoggedIn(req) - isProjectPage = !!req.path.match('^/project/[a-f0-9]{24}$') - - helmet({ # note that more headers are added by default - dnsPrefetchControl: false - referrerPolicy: { policy: 'origin-when-cross-origin' } - noCache: isLoggedIn || isProjectPage - noSniff: false - hsts: false - frameguard: false - })(req, res, next) - -profiler = require "v8-profiler-node8" -privateApiRouter.get "/profile", (req, res) -> - time = parseInt(req.query.time || "1000") - profiler.startProfiling("test") - setTimeout () -> - profile = profiler.stopProfiling("test") - res.json(profile) - , time - -privateApiRouter.get "/heapdump", (req, res)-> - require('heapdump').writeSnapshot '/tmp/' + Date.now() + '.web.heapsnapshot', (err, filename)-> - res.send filename - -logger.info ("creating HTTP server").yellow -server = require('http').createServer(app) - -# provide settings for separate web and api processes -# if enableApiRouter and enableWebRouter are not defined they default -# to true. -notDefined = (x) -> !x? -enableApiRouter = Settings.web?.enableApiRouter -if enableApiRouter or notDefined(enableApiRouter) - logger.info("providing api router"); - app.use(privateApiRouter) - app.use(ErrorController.handleApiError) - -enableWebRouter = Settings.web?.enableWebRouter -if enableWebRouter or notDefined(enableWebRouter) - logger.info("providing web router"); - app.use(publicApiRouter) # public API goes with web router for public access - app.use(ErrorController.handleApiError) - app.use(webRouter) - app.use(ErrorController.handleError) - -metrics.injectMetricsRoute(webRouter) - -router = new Router(webRouter, privateApiRouter, publicApiRouter) - -module.exports = - app: app - server: server diff --git a/services/web/app/coffee/infrastructure/Sixpack.coffee b/services/web/app/coffee/infrastructure/Sixpack.coffee deleted file mode 100644 index 769300c397..0000000000 --- a/services/web/app/coffee/infrastructure/Sixpack.coffee +++ /dev/null @@ -1,104 +0,0 @@ -settings = require("settings-sharelatex") -request = require("request") -logger = require("logger-sharelatex") - - - -timeout = if process.env.NODE_ENV == "production" then 500 else 5000 -logger.log "using timeout of #{timeout}ms for sixpack server calls" - -generate_client_id = -> - # from http://stackoverflow.com/questions/105034 - 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) -> - r = Math.random() * 16 | 0 - v = if c == 'x' then r else r & 0x3 | 0x8 - v.toString 16 - -_request_uri = (endpoint, params) -> - query_string = [] - e = encodeURIComponent - for key of params - if params.hasOwnProperty(key) - vals = params[key] - if Object::toString.call(vals) != '[object Array]' - vals = [ vals ] - i = 0 - while i < vals.length - query_string.push e(key) + '=' + e(vals[i]) - i += 1 - if query_string.length - endpoint += '?' + query_string.join('&') - endpoint - -_request = (uri, params, callback)-> - opts = - uri:_request_uri(uri, params) - json:true - timeout:timeout - request.get opts, (err, res, body)-> - callback err, body - -module.exports = sixpack = - - client: (user_id)-> - client = new sixpack.Session(user_id, settings.apis.sixpack.url) - return client - - Session : (client_id, base_url, ip_address, user_agent) -> - - @client_id = client_id or sixpack.generate_client_id() - @base_url = base_url or sixpack.base_url - - participate: (experiment_name, alternatives, force, callback) => - if typeof force == 'function' - callback = force - force = null - if !/^[a-z0-9][a-z0-9\-_ ]*$/.test(experiment_name) - return callback(new Error('Bad experiment_name')) - if alternatives.length < 2 - return callback(new Error('Must specify at least 2 alternatives')) - i = 0 - while i < alternatives.length - if !/^[a-z0-9][a-z0-9\-_ ]*$/.test(alternatives[i]) - return callback(new Error('Bad alternative name: ' + alternatives[i])) - i += 1 - params = - client_id: @client_id - experiment: experiment_name - alternatives: alternatives - - if force != null and _in_array(alternatives, force) - return callback(null, - 'status': 'ok' - 'alternative': 'name': force - 'experiment': - 'version': 0 - 'name': experiment_name - 'client_id': @client_id) - - _request @base_url + '/participate', params, (err, res) -> - if err? - res = - status: 'failed' - error: err - alternative: name: alternatives[0] - callback null, res - - convert: (experiment_name, callback)=> - if !/^[a-z0-9][a-z0-9\-_ ]*$/.test(experiment_name) - return callback(new Error('Bad experiment_name')) - params = - client_id: @client_id - experiment: experiment_name - if @ip_address - params.ip_address = @ip_address - if @user_agent - params.user_agent = @user_agent - _request @base_url + '/convert', params, (err, res) -> - if err? - res = - status: 'failed' - error: err - callback null, res - - diff --git a/services/web/app/coffee/infrastructure/mongojs.coffee b/services/web/app/coffee/infrastructure/mongojs.coffee deleted file mode 100644 index 4bf6a2b970..0000000000 --- a/services/web/app/coffee/infrastructure/mongojs.coffee +++ /dev/null @@ -1,6 +0,0 @@ -Settings = require "settings-sharelatex" -mongojs = require "mongojs" -db = mongojs(Settings.mongo.url, ["projects", "users", "userstubs", "tokens", "docSnapshots", "projectHistoryFailures"]) -module.exports = - db: db - ObjectId: mongojs.ObjectId diff --git a/services/web/app/coffee/models/DeletedProject.coffee b/services/web/app/coffee/models/DeletedProject.coffee deleted file mode 100644 index 7a2fdc16ad..0000000000 --- a/services/web/app/coffee/models/DeletedProject.coffee +++ /dev/null @@ -1,27 +0,0 @@ -mongoose = require('mongoose') -Settings = require 'settings-sharelatex' -ProjectSchema = require('./Project.js').ProjectSchema - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -DeleterDataSchema = new Schema - deleterId: {type: ObjectId, ref: 'User'} - deleterIpAddress: { type: String } - deletedAt: { type: Date } - -DeletedProjectSchema = new Schema({ - deleterData : [DeleterDataSchema] - project: [ProjectSchema] -}, collection: 'deletedProjects') - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -DeletedProject = conn.model('DeletedProject', DeletedProjectSchema) - -mongoose.model 'DeletedProject', DeletedProjectSchema -exports.DeletedProject = DeletedProject -exports.DeletedProjectSchema = DeletedProjectSchema diff --git a/services/web/app/coffee/models/Doc.coffee b/services/web/app/coffee/models/Doc.coffee deleted file mode 100644 index 862638acb7..0000000000 --- a/services/web/app/coffee/models/Doc.coffee +++ /dev/null @@ -1,13 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -DocSchema = new Schema - name : {type:String, default:'new doc'} - - -mongoose.model 'Doc', DocSchema -exports.Doc = mongoose.model 'Doc' -exports.DocSchema = DocSchema diff --git a/services/web/app/coffee/models/File.coffee b/services/web/app/coffee/models/File.coffee deleted file mode 100644 index 121a8cabb6..0000000000 --- a/services/web/app/coffee/models/File.coffee +++ /dev/null @@ -1,16 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -FileSchema = new Schema - name : type:String, default:'' - created : type:Date, default: () -> new Date() - rev : {type:Number, default:0} - linkedFileData: { type: Schema.Types.Mixed } - hash : type:String - -mongoose.model 'File', FileSchema -exports.File = mongoose.model 'File' -exports.FileSchema = FileSchema diff --git a/services/web/app/coffee/models/Folder.coffee b/services/web/app/coffee/models/Folder.coffee deleted file mode 100644 index 4c2bf04b64..0000000000 --- a/services/web/app/coffee/models/Folder.coffee +++ /dev/null @@ -1,20 +0,0 @@ -mongoose = require('mongoose') -Settings = require 'settings-sharelatex' -DocSchema = require('./Doc').DocSchema -FileSchema = require('./File').FileSchema - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -FolderSchema = new Schema - name : {type:String, default:'new folder'} - -FolderSchema.add - docs : [DocSchema] - fileRefs : [FileSchema] - folders : [FolderSchema] - - -mongoose.model('Folder', FolderSchema) -exports.Folder = mongoose.model('Folder') -exports.FolderSchema = FolderSchema diff --git a/services/web/app/coffee/models/Institution.coffee b/services/web/app/coffee/models/Institution.coffee deleted file mode 100644 index f025b00ea2..0000000000 --- a/services/web/app/coffee/models/Institution.coffee +++ /dev/null @@ -1,37 +0,0 @@ -mongoose = require 'mongoose' -Schema = mongoose.Schema -ObjectId = Schema.ObjectId -settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' -request = require 'request' - -InstitutionSchema = new Schema - v1Id: { type: Number, required: true } - managerIds: [ type:ObjectId, ref:'User' ] - metricsEmail: { - optedOutUserIds: [ type:ObjectId, ref:'User' ] - lastSent: { type : Date } - } - -# fetch institution's data from v1 API. Errors are ignored -InstitutionSchema.method 'fetchV1Data', (callback = (error, institution)->) -> - url = "#{settings.apis.v1.url}/universities/list/#{this.v1Id}" - request.get url, (error, response, body) => - try - parsedBody = JSON.parse(body) - catch error # log error and carry on without v1 data - logger.err { model: 'Institution', v1Id: this.v1Id, error }, '[fetchV1DataError]' - this.name = parsedBody?.name - this.countryCode = parsedBody?.country_code - this.departments = parsedBody?.departments - this.portalSlug = parsedBody?.portal_slug - callback(null, this) - -conn = mongoose.createConnection(settings.mongo.url, { - server: {poolSize: settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -Institution = conn.model 'Institution', InstitutionSchema -exports.Institution = Institution -exports.InstitutionSchema = InstitutionSchema diff --git a/services/web/app/coffee/models/OauthAccessToken.coffee b/services/web/app/coffee/models/OauthAccessToken.coffee deleted file mode 100644 index 22f68c743a..0000000000 --- a/services/web/app/coffee/models/OauthAccessToken.coffee +++ /dev/null @@ -1,31 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -OauthAccessTokenSchema = new Schema( - { - accessToken: String - accessTokenExpiresAt: Date - oauthApplication_id: { type: ObjectId, ref: 'OauthApplication' } - refreshToken: String - refreshTokenExpiresAt: Date - scope: String - user_id: { type: ObjectId, ref: 'User' } - }, - { - collection: 'oauthAccessTokens' - } -) - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -OauthAccessToken = conn.model('OauthAccessToken', OauthAccessTokenSchema) - -mongoose.model 'OauthAccessToken', OauthAccessTokenSchema -exports.OauthAccessToken = OauthAccessToken -exports.OauthAccessTokenSchema = OauthAccessTokenSchema diff --git a/services/web/app/coffee/models/OauthApplication.coffee b/services/web/app/coffee/models/OauthApplication.coffee deleted file mode 100644 index af68376d45..0000000000 --- a/services/web/app/coffee/models/OauthApplication.coffee +++ /dev/null @@ -1,30 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -OauthApplicationSchema = new Schema( - { - id: String - clientSecret: String - grants: [ String ] - name: String - redirectUris: [ String ] - scopes: [ String ] - }, - { - collection: 'oauthApplications' - } -) - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -OauthApplication = conn.model('OauthApplication', OauthApplicationSchema) - -mongoose.model 'OauthApplication', OauthApplicationSchema -exports.OauthApplication = OauthApplication -exports.OauthApplicationSchema = OauthApplicationSchema diff --git a/services/web/app/coffee/models/OauthAuthorizationCode.coffee b/services/web/app/coffee/models/OauthAuthorizationCode.coffee deleted file mode 100644 index a37a469a1e..0000000000 --- a/services/web/app/coffee/models/OauthAuthorizationCode.coffee +++ /dev/null @@ -1,30 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -OauthAuthorizationCodeSchema = new Schema( - { - authorizationCode: String - expiresAt: Date - oauthApplication_id: { type: ObjectId, ref: 'OauthApplication' } - redirectUri: String - scope: String - user_id: { type: ObjectId, ref: 'User' } - }, - { - collection: 'oauthAuthorizationCodes' - } -) - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -OauthAuthorizationCode = conn.model('OauthAuthorizationCode', OauthAuthorizationCodeSchema) - -mongoose.model 'OauthAuthorizationCode', OauthAuthorizationCodeSchema -exports.OauthAuthorizationCode = OauthAuthorizationCode -exports.OauthAuthorizationCodeSchema = OauthAuthorizationCodeSchema diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee deleted file mode 100644 index 0a46935d82..0000000000 --- a/services/web/app/coffee/models/Project.coffee +++ /dev/null @@ -1,115 +0,0 @@ -mongoose = require('mongoose') -Settings = require 'settings-sharelatex' -_ = require('underscore') -FolderSchema = require('./Folder.js').FolderSchema -logger = require('logger-sharelatex') -concreteObjectId = require('mongoose').Types.ObjectId -Errors = require "../Features/Errors/Errors" - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -DeletedDocSchema = new Schema - name: String - deletedAt: {type: Date} - -DeletedFileSchema = new Schema - name : String - created : type:Date - linkedFileData : { type: Schema.Types.Mixed } - hash : type:String - deletedAt : {type: Date} - -ProjectSchema = new Schema - name : {type:String, default:'new project'} - lastUpdated : {type:Date, default: () -> new Date()} - lastUpdatedBy : {type:ObjectId, ref: 'User'} - lastOpened : {type:Date} - active : { type: Boolean, default: true } - owner_ref : {type:ObjectId, ref:'User'} - collaberator_refs : [ type:ObjectId, ref:'User' ] - readOnly_refs : [ type:ObjectId, ref:'User' ] - rootDoc_id : {type: ObjectId} - rootFolder : [FolderSchema] - version : {type: Number} # incremented for every change in the project structure (folders and filenames) - publicAccesLevel : {type: String, default: 'private'} - compiler : {type:String, default:'pdflatex'} - spellCheckLanguage : {type:String, default:'en'} - deletedByExternalDataSource : {type: Boolean, default: false} - description : {type:String, default:''} - archived : { type: Boolean } - deletedDocs : [DeletedDocSchema] - deletedFiles : [DeletedFileSchema] - imageName : { type: String } - brandVariationId : { type: String } - track_changes : { type: Object } - tokens : - readOnly : { - type: String, - index: { - unique: true, - partialFilterExpression: {'tokens.readOnly': {$exists: true}} - } - } - readAndWrite : { - type: String, - index: { - unique: true, - partialFilterExpression: {'tokens.readAndWrite': {$exists: true}} - } - } - readAndWritePrefix: { - type: String, - index: { - unique: true, - partialFilterExpression: {'tokens.readAndWritePrefix': {$exists: true}} - } - } - tokenAccessReadOnly_refs : [ type:ObjectId, ref:'User' ] - tokenAccessReadAndWrite_refs : [ type:ObjectId, ref:'User' ] - fromV1TemplateId: { type: Number } - fromV1TemplateVersionId: { type: Number } - overleaf : - id : { type: Number } - imported_at_ver_id : { type: Number } - token : { type: String } - read_token : { type: String } - history : - id : { type: Number } - display : { type: Boolean } - upgradedAt : { type: Date } - collabratecUsers : [ - { - user_id : { type: ObjectId, ref:'User' } - collabratec_document_id : { type: String } - collabratec_privategroup_id : { type: String } - added_at : { type: Date, default: () -> new Date() } - } - ] - -ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> - if project_or_id._id? - callback null, project_or_id - else - try - concreteObjectId(project_or_id.toString()) - catch e - return callback(new Errors.NotFoundError(e.message)) - this.findById project_or_id, fields, callback - -applyToAllFilesRecursivly = ProjectSchema.statics.applyToAllFilesRecursivly = (folder, fun)-> - _.each folder.fileRefs, (file)-> - fun(file) - _.each folder.folders, (folder)-> - applyToAllFilesRecursivly(folder, fun) - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -Project = conn.model('Project', ProjectSchema) - -mongoose.model 'Project', ProjectSchema -exports.Project = Project -exports.ProjectSchema = ProjectSchema diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee deleted file mode 100644 index 3be74f63ac..0000000000 --- a/services/web/app/coffee/models/ProjectInvite.coffee +++ /dev/null @@ -1,43 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - - -EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30 - -ExpiryDate = () -> - timestamp = new Date() - timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS) - return timestamp - - - -ProjectInviteSchema = new Schema( - { - email: String - token: String - sendingUserId: ObjectId - projectId: ObjectId - privileges: String - createdAt: {type: Date, default: Date.now} - expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}} - }, - { - collection: 'projectInvites' - } -) - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema) - -mongoose.model 'ProjectInvite', ProjectInviteSchema -exports.ProjectInvite = ProjectInvite -exports.ProjectInviteSchema = ProjectInviteSchema -exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS diff --git a/services/web/app/coffee/models/Publisher.coffee b/services/web/app/coffee/models/Publisher.coffee deleted file mode 100644 index e4496b0d42..0000000000 --- a/services/web/app/coffee/models/Publisher.coffee +++ /dev/null @@ -1,38 +0,0 @@ -mongoose = require 'mongoose' -Schema = mongoose.Schema -ObjectId = Schema.ObjectId -settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' -request = require 'request' - -PublisherSchema = new Schema - slug: { type: String, required: true } - managerIds: [ type:ObjectId, ref:'User' ] - -# fetch publisher's (brand on v1) data from v1 API. Errors are ignored -PublisherSchema.method 'fetchV1Data', (callback = (error, publisher)->) -> - request { - baseUrl: settings.apis.v1.url - url: "/api/v2/brands/#{this.slug}" - method: 'GET' - auth: - user: settings.apis.v1.user - pass: settings.apis.v1.pass - sendImmediately: true - }, (error, response, body) => - try - parsedBody = JSON.parse(body) - catch error # log error and carry on without v1 data - logger.err { model: 'Publisher', slug: this.slug, error }, '[fetchV1DataError]' - this.name = parsedBody?.name - this.partner = parsedBody?.partner - callback(null, this) - -conn = mongoose.createConnection(settings.mongo.url, { - server: {poolSize: settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -Publisher = conn.model 'Publisher', PublisherSchema -exports.Publisher = Publisher -exports.PublisherSchema = PublisherSchema diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee deleted file mode 100644 index d094f88971..0000000000 --- a/services/web/app/coffee/models/Subscription.coffee +++ /dev/null @@ -1,47 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' -TeamInviteSchema = require('./TeamInvite').TeamInviteSchema - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -SubscriptionSchema = new Schema - admin_id : {type:ObjectId, ref:'User', index: {unique: true, dropDups: true}} - manager_ids : [ type:ObjectId, ref:'User' ] - member_ids : [ type:ObjectId, ref:'User' ] - invited_emails: [ String ] - teamInvites : [ TeamInviteSchema ] - recurlySubscription_id : String - teamName : {type: String} - teamNotice : {type: String} - planCode : {type: String} - groupPlan : {type: Boolean, default: false} - membersLimit: {type:Number, default:0} - customAccount: Boolean - overleaf: - id: - type: Number - index: - unique: true, - partialFilterExpression: {'overleaf.id': {$exists: true}} - - -SubscriptionSchema.statics.findAndModify = (query, update, callback)-> - self = @ - this.update query, update, -> - self.findOne query, callback - -# Subscriptions have no v1 data to fetch -SubscriptionSchema.method 'fetchV1Data', (callback = (error, subscription)->) -> - callback(null, this) - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -Subscription = conn.model('Subscription', SubscriptionSchema) - -mongoose.model 'Subscription', SubscriptionSchema -exports.Subscription = Subscription -exports.SubscriptionSchema = SubscriptionSchema diff --git a/services/web/app/coffee/models/SystemMessage.coffee b/services/web/app/coffee/models/SystemMessage.coffee deleted file mode 100644 index ccaa2028e2..0000000000 --- a/services/web/app/coffee/models/SystemMessage.coffee +++ /dev/null @@ -1,16 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -SystemMessageSchema = new Schema - content : type: String, default:'' - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - - -exports.SystemMessage = conn.model('SystemMessage', SystemMessageSchema) diff --git a/services/web/app/coffee/models/TeamInvite.coffee b/services/web/app/coffee/models/TeamInvite.coffee deleted file mode 100644 index e488724745..0000000000 --- a/services/web/app/coffee/models/TeamInvite.coffee +++ /dev/null @@ -1,15 +0,0 @@ -mongoose = require 'mongoose' -Settings = require 'settings-sharelatex' - -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -TeamInviteSchema = new Schema - email : { type: String, required: true } - token : { type: String } - inviterName : { type: String } - sentAt : { type: Date } - -mongoose.model 'TeamInvite', TeamInviteSchema -exports.TeamInvite = mongoose.model 'TeamInvite' -exports.TeamInviteSchema = TeamInviteSchema diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee deleted file mode 100644 index d0f76a4d27..0000000000 --- a/services/web/app/coffee/models/User.coffee +++ /dev/null @@ -1,92 +0,0 @@ -Project = require('./Project').Project -Settings = require 'settings-sharelatex' -_ = require('underscore') -mongoose = require('mongoose') -uuid = require('uuid') -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -# See https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/574698#574698 -MAX_EMAIL_LENGTH = 254 - -UserSchema = new Schema - email : {type : String, default : '', maxlength: MAX_EMAIL_LENGTH } - emails: [{ - email: { type : String, default : '', maxlength: MAX_EMAIL_LENGTH }, - reversedHostname: { type : String, default : '' }, - createdAt: { type : Date, default: () -> new Date() }, - confirmedAt: { type: Date } - }], - first_name : {type : String, default : ''} - last_name : {type : String, default : ''} - role : {type : String, default : ''} - institution : {type : String, default : ''} - hashedPassword : String - isAdmin : {type : Boolean, default : false} - staffAccess: { - publisherMetrics: {type : Boolean, default: false} - publisherManagement: {type : Boolean, default: false} - institutionMetrics: {type : Boolean, default: false} - institutionManagement: {type : Boolean, default: false} - groupMetrics: {type : Boolean, default: false} - groupManagement: {type : Boolean, default: false} - } - signUpDate : {type : Date, default: () -> new Date() } - lastLoggedIn : {type : Date} - lastLoginIp : {type : String, default : ''} - loginCount : {type : Number, default: 0} - holdingAccount : {type : Boolean, default: false} - ace : { - mode : {type : String, default: 'none'} - theme : {type : String, default: 'textmate'} - overallTheme : {type: String, default: "" } - fontSize : {type : Number, default:'12'} - autoComplete : {type : Boolean, default: true} - autoPairDelimiters : {type : Boolean, default: true} - spellCheckLanguage : {type : String, default: "en"} - pdfViewer : {type : String, default: "pdfjs"} - syntaxValidation : {type : Boolean} - fontFamily : {type : String} - lineHeight : {type : String} - } - features : { - collaborators: { type:Number, default: Settings.defaultFeatures.collaborators } - versioning: { type:Boolean, default: Settings.defaultFeatures.versioning } - dropbox: { type:Boolean, default: Settings.defaultFeatures.dropbox } - github: { type:Boolean, default: Settings.defaultFeatures.github } - gitBridge: { type:Boolean, default: Settings.defaultFeatures.gitBridge } - compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout } - compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup } - templates: { type:Boolean, default: Settings.defaultFeatures.templates } - references: { type:Boolean, default: Settings.defaultFeatures.references } - trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges } - mendeley: { type:Boolean, default: Settings.defaultFeatures.mendeley } - zotero: { type:Boolean, default: Settings.defaultFeatures.zotero } - referencesSearch: { type:Boolean, default: Settings.defaultFeatures.referencesSearch } - } - must_reconfirm:{ type:Boolean, default: false } - referal_id : {type:String, default:() -> uuid.v4().split("-")[0]} - refered_users: [ type:ObjectId, ref:'User' ] - refered_user_count: { type:Number, default: 0 } - refProviders: { - mendeley: Boolean # coerce the refProviders values to Booleans - zotero: Boolean - } - betaProgram: { type:Boolean, default: false} - overleaf: - id: { type: Number } - accessToken: { type: String } - refreshToken: { type: String } - awareOfV2: { type:Boolean, default: false } - thirdPartyIdentifiers: { type: Array, default: [] } - migratedAt: { type: Date } - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -User = conn.model('User', UserSchema) - -model = mongoose.model 'User', UserSchema -exports.User = User diff --git a/services/web/app/coffee/models/UserStub.coffee b/services/web/app/coffee/models/UserStub.coffee deleted file mode 100644 index 70dd1e60dc..0000000000 --- a/services/web/app/coffee/models/UserStub.coffee +++ /dev/null @@ -1,22 +0,0 @@ -Settings = require "settings-sharelatex" -mongoose = require('mongoose') -Schema = mongoose.Schema -ObjectId = Schema.ObjectId - -UserStubSchema = new Schema - email : { type : String, default : '' } - first_name : { type : String, default : '' } - last_name : { type : String, default : '' } - overleaf : { id: { type: Number } } - thirdPartyIdentifiers: { type: Array, default: [] } - confirmed_at: Date - -conn = mongoose.createConnection(Settings.mongo.url, { - server: {poolSize: Settings.mongo.poolSize || 10}, - config: {autoIndex: false} -}) - -UserStub = conn.model('UserStub', UserStubSchema) - -model = mongoose.model 'UserStub', UserStubSchema -exports.UserStub = UserStub diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee deleted file mode 100644 index afb7c76a47..0000000000 --- a/services/web/app/coffee/router.coffee +++ /dev/null @@ -1,543 +0,0 @@ -AdminController = require('./Features/ServerAdmin/AdminController') -ErrorController = require('./Features/Errors/ErrorController') -ProjectController = require("./Features/Project/ProjectController") -ProjectApiController = require("./Features/Project/ProjectApiController") -SpellingController = require('./Features/Spelling/SpellingController') -EditorController = require("./Features/Editor/EditorController") -EditorRouter = require("./Features/Editor/EditorRouter") -Settings = require('settings-sharelatex') -TpdsController = require('./Features/ThirdPartyDataStore/TpdsController') -SubscriptionRouter = require './Features/Subscription/SubscriptionRouter' -UploadsRouter = require './Features/Uploads/UploadsRouter' -metrics = require('metrics-sharelatex') -ReferalController = require('./Features/Referal/ReferalController') -AuthenticationController = require('./Features/Authentication/AuthenticationController') -TagsController = require("./Features/Tags/TagsController") -NotificationsController = require("./Features/Notifications/NotificationsController") -CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter') -UserInfoController = require('./Features/User/UserInfoController') -UserController = require("./Features/User/UserController") -UserEmailsController = require("./Features/User/UserEmailsController") -UserPagesController = require('./Features/User/UserPagesController') -DocumentController = require('./Features/Documents/DocumentController') -CompileManager = require("./Features/Compile/CompileManager") -CompileController = require("./Features/Compile/CompileController") -ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")(Settings.apis.clsi?.backendGroupName) -HealthCheckController = require("./Features/HealthCheck/HealthCheckController") -ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" -FileStoreController = require("./Features/FileStore/FileStoreController") -HistoryController = require("./Features/History/HistoryController") -ExportsController = require("./Features/Exports/ExportsController") -PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter") -StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter") -ChatController = require("./Features/Chat/ChatController") -BlogController = require("./Features/Blog/BlogController") -Modules = require "./infrastructure/Modules" -RateLimiterMiddleware = require('./Features/Security/RateLimiterMiddleware') -CooldownMiddleware = require('./Features/Cooldown/CooldownMiddleware') -RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter') -InactiveProjectController = require("./Features/InactiveData/InactiveProjectController") -ContactRouter = require("./Features/Contacts/ContactRouter") -ReferencesController = require('./Features/References/ReferencesController') -AuthorizationMiddleware = require('./Features/Authorization/AuthorizationMiddleware') -BetaProgramController = require('./Features/BetaProgram/BetaProgramController') -SudoModeController = require('./Features/SudoMode/SudoModeController') -SudoModeMiddleware = require('./Features/SudoMode/SudoModeMiddleware') -AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') -AnnouncementsController = require("./Features/Announcements/AnnouncementsController") -MetaController = require('./Features/Metadata/MetaController') -TokenAccessController = require('./Features/TokenAccess/TokenAccessController') -Features = require('./infrastructure/Features') -LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter' -TemplatesRouter = require './Features/Templates/TemplatesRouter' -InstitutionsController = require './Features/Institutions/InstitutionsController' -UserMembershipRouter = require './Features/UserMembership/UserMembershipRouter' - -logger = require("logger-sharelatex") -_ = require("underscore") - -module.exports = class Router - constructor: (webRouter, privateApiRouter, publicApiRouter)-> - if !Settings.allowPublicAccess - webRouter.all '*', AuthenticationController.requireGlobalLogin - - - webRouter.get '/login', UserPagesController.loginPage - AuthenticationController.addEndpointToLoginWhitelist '/login' - - webRouter.post '/login', AuthenticationController.passportLogin - - webRouter.get '/logout', UserPagesController.logoutPage - webRouter.post '/logout', UserController.logout - - webRouter.get '/restricted', AuthorizationMiddleware.restricted - - - if Features.hasFeature('registration') - webRouter.get '/register', UserPagesController.registerPage - AuthenticationController.addEndpointToLoginWhitelist '/register' - - EditorRouter.apply(webRouter, privateApiRouter) - CollaboratorsRouter.apply(webRouter, privateApiRouter) - SubscriptionRouter.apply(webRouter, privateApiRouter, publicApiRouter) - UploadsRouter.apply(webRouter, privateApiRouter) - PasswordResetRouter.apply(webRouter, privateApiRouter) - StaticPagesRouter.apply(webRouter, privateApiRouter) - RealTimeProxyRouter.apply(webRouter, privateApiRouter) - ContactRouter.apply(webRouter, privateApiRouter) - AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter) - LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter) - TemplatesRouter.apply(webRouter) - UserMembershipRouter.apply(webRouter) - - Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter) - - if Settings.enableSubscriptions - webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus - - if !Settings.overleaf? - webRouter.get '/blog', BlogController.getIndexPage - webRouter.get '/blog/*', BlogController.getPage - - webRouter.get '/user/activate', UserPagesController.activateAccountPage - AuthenticationController.addEndpointToLoginWhitelist '/user/activate' - - webRouter.get '/user/settings', - AuthenticationController.requireLogin(), - SudoModeMiddleware.protectPage, - UserPagesController.settingsPage - webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings - webRouter.post '/user/password/update', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: "change-password" - maxRequests: 10 - timeInterval: 60 - }), - UserController.changePassword - webRouter.get '/user/emails', - AuthenticationController.requireLogin(), - UserEmailsController.list - webRouter.get '/user/emails/confirm', - UserEmailsController.showConfirm - webRouter.post '/user/emails/confirm', - RateLimiterMiddleware.rateLimit({ - endpointName: "confirm-email" - maxRequests: 10 - timeInterval: 60 - }), - UserEmailsController.confirm - webRouter.post '/user/emails/resend_confirmation', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: "resend-confirmation" - maxRequests: 10 - timeInterval: 60 - }), - UserEmailsController.resendConfirmation - - if Features.hasFeature 'affiliations' - webRouter.post '/user/emails', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: 'add-email', - maxRequests: 10 - timeInterval: 60 - }), - UserEmailsController.add - webRouter.post '/user/emails/delete', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: 'delete-email', - maxRequests: 10 - timeInterval: 60 - }), - UserEmailsController.remove - webRouter.post '/user/emails/default', - AuthenticationController.requireLogin(), - UserEmailsController.setDefault - webRouter.post '/user/emails/endorse', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: "endorse-email" - maxRequests: 30 - timeInterval: 60 - }), - UserEmailsController.endorse - - - webRouter.get '/user/sessions', - AuthenticationController.requireLogin(), - SudoModeMiddleware.protectPage, - UserPagesController.sessionsPage - webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions - - webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe - webRouter.post '/user/delete', - RateLimiterMiddleware.rateLimit({ - endpointName: "delete-user" - maxRequests: 10 - timeInterval: 60 - }), - AuthenticationController.requireLogin(), - UserController.tryDeleteUser - - webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo - privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo - - webRouter.get '/user/reconfirm', UserPagesController.renderReconfirmAccountPage - # for /user/reconfirm POST, see password router - - webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson - webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.projectEntitiesJson - - webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage - webRouter.post '/project/new', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit({ - endpointName: "create-project" - maxRequests: 20 - timeInterval: 60 - }), ProjectController.newProject - - webRouter.get '/Project/:Project_id', RateLimiterMiddleware.rateLimit({ - endpointName: "open-project" - params: ["Project_id"] - maxRequests: 15 - timeInterval: 60 - }), AuthorizationMiddleware.ensureUserCanReadProject, ProjectController.loadEditor - webRouter.get '/Project/:Project_id/file/:File_id', AuthorizationMiddleware.ensureUserCanReadProject, FileStoreController.getFile - webRouter.post '/project/:Project_id/settings', AuthorizationMiddleware.ensureUserCanWriteProjectSettings, ProjectController.updateProjectSettings - webRouter.post '/project/:Project_id/settings/admin', AuthorizationMiddleware.ensureUserCanAdminProject, ProjectController.updateProjectAdminSettings - - webRouter.post '/project/:Project_id/compile', RateLimiterMiddleware.rateLimit({ - endpointName: "compile-project-http" - params: ["Project_id"] - maxRequests: 800 - timeInterval: 60 * 60 - }), AuthorizationMiddleware.ensureUserCanReadProject, CompileController.compile - - webRouter.post '/project/:Project_id/compile/stop', AuthorizationMiddleware.ensureUserCanReadProject, CompileController.stopCompile - - # LEGACY: Used by the web download buttons, adds filename header, TODO: remove at some future date - webRouter.get '/project/:Project_id/output/output.pdf', AuthorizationMiddleware.ensureUserCanReadProject, CompileController.downloadPdf - - # PDF Download button - webRouter.get /^\/download\/project\/([^\/]*)\/output\/output\.pdf$/, - ((req, res, next) -> - params = - "Project_id": req.params[0] - req.params = params - next() - ), AuthorizationMiddleware.ensureUserCanReadProject, CompileController.downloadPdf - - # PDF Download button for specific build - webRouter.get /^\/download\/project\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/output\.pdf$/, - ((req, res, next) -> - params = - "Project_id": req.params[0] - "build_id": req.params[1] - req.params = params - next() - ), AuthorizationMiddleware.ensureUserCanReadProject, CompileController.downloadPdf - - # Used by the pdf viewers - webRouter.get /^\/project\/([^\/]*)\/output\/(.*)$/, - ((req, res, next) -> - params = - "Project_id": req.params[0] - "file": req.params[1] - req.params = params - next() - ), AuthorizationMiddleware.ensureUserCanReadProject, CompileController.getFileFromClsi - # direct url access to output files for a specific build (query string not required) - webRouter.get /^\/project\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, - ((req, res, next) -> - params = - "Project_id": req.params[0] - "build_id": req.params[1] - "file": req.params[2] - req.params = params - next() - ), AuthorizationMiddleware.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() - ), AuthorizationMiddleware.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() - ), AuthorizationMiddleware.ensureUserCanReadProject, CompileController.getFileFromClsi - - - webRouter.delete "/project/:Project_id/output", AuthorizationMiddleware.ensureUserCanReadProject, CompileController.deleteAuxFiles - webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddleware.ensureUserCanReadProject, CompileController.proxySyncCode - webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddleware.ensureUserCanReadProject, CompileController.proxySyncPdf - webRouter.get "/project/:Project_id/wordcount", AuthorizationMiddleware.ensureUserCanReadProject, CompileController.wordCount - - webRouter.delete '/Project/:Project_id', AuthorizationMiddleware.ensureUserCanAdminProject, ProjectController.deleteProject - webRouter.post '/Project/:Project_id/restore', AuthorizationMiddleware.ensureUserCanAdminProject, ProjectController.restoreProject - webRouter.post '/Project/:Project_id/clone', AuthorizationMiddleware.ensureUserCanReadProject, ProjectController.cloneProject - - webRouter.post '/project/:Project_id/rename', AuthorizationMiddleware.ensureUserCanAdminProject, ProjectController.renameProject - - webRouter.get "/project/:Project_id/updates", AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails - webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi - webRouter.get "/project/:Project_id/diff", AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails - webRouter.get "/project/:Project_id/filetree/diff", AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi - webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddleware.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi - webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddleware.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc - webRouter.post "/project/:project_id/restore_file", AuthorizationMiddleware.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 - webRouter.get "/project/:project_id/version/:version/zip", AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.downloadZipOfVersion - privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory - - webRouter.get "/project/:Project_id/labels", AuthorizationMiddleware.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels - webRouter.post "/project/:Project_id/labels", AuthorizationMiddleware.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel - webRouter.delete "/project/:Project_id/labels/:label_id", AuthorizationMiddleware.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.deleteLabel - - webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, ExportsController.exportProject - webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, ExportsController.exportStatus - webRouter.get '/project/:project_id/export/:export_id/:type', AuthorizationMiddleware.ensureUserCanWriteProjectContent, ExportsController.exportDownload - - webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddleware.ensureUserCanReadProject, ProjectDownloadsController.downloadProject - webRouter.get '/project/download/zip', AuthorizationMiddleware.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects - - webRouter.get '/project/:project_id/metadata', AuthorizationMiddleware.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.getMetadata - webRouter.post '/project/:project_id/doc/:doc_id/metadata', AuthorizationMiddleware.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.broadcastMetadataForDoc - - - webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags - webRouter.post '/tag', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit({ - endpointName: "create-tag" - maxRequests: 30 - timeInterval: 60 - }), TagsController.createTag - webRouter.post '/tag/:tag_id/rename', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit({ - endpointName: "rename-tag" - maxRequests: 30 - timeInterval: 60 - }), TagsController.renameTag - webRouter.delete '/tag/:tag_id', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit({ - endpointName: "delete-tag" - maxRequests: 30 - timeInterval: 60 - }), TagsController.deleteTag - webRouter.post '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit({ - endpointName: "add-project-to-tag" - maxRequests: 30 - timeInterval: 60 - }), TagsController.addProjectToTag - webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), RateLimiterMiddleware.rateLimit({ - endpointName: "remove-project-from-tag" - maxRequests: 30 - timeInterval: 60 - }), TagsController.removeProjectFromTag - - webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications - webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead - - webRouter.get '/announcements', AuthenticationController.requireLogin(), AnnouncementsController.getUndreadAnnouncements - - - # Deprecated in favour of /internal/project/:project_id but still used by versioning - privateApiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails - - # New 'stable' /internal API end points - privateApiRouter.get '/internal/project/:project_id', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails - privateApiRouter.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject - privateApiRouter.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf - - privateApiRouter.post '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects - privateApiRouter.post '/internal/project/:project_id/deactivate', AuthenticationController.httpAuth, InactiveProjectController.deactivateProject - - webRouter.get /^\/internal\/project\/([^\/]*)\/output\/(.*)$/, - ((req, res, next) -> - params = - "Project_id": req.params[0] - "file": req.params[1] - req.params = params - next() - ), AuthenticationController.httpAuth, CompileController.getFileFromClsi - - privateApiRouter.get '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument - privateApiRouter.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument - - privateApiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate - privateApiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate - - privateApiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents - privateApiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents - - webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - - webRouter.get "/project/:project_id/messages", AuthorizationMiddleware.ensureUserCanReadProject, ChatController.getMessages - webRouter.post "/project/:project_id/messages", AuthorizationMiddleware.ensureUserCanReadProject, RateLimiterMiddleware.rateLimit({ - endpointName: "send-chat-message" - maxRequests: 100 - timeInterval: 60 - }), ChatController.sendMessage - - webRouter.post "/project/:Project_id/references/index", AuthorizationMiddleware.ensureUserCanReadProject, RateLimiterMiddleware.rateLimit({ - endpointName: "index-project-references" - maxRequests: 30 - timeInterval: 60 - }), ReferencesController.index - webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddleware.ensureUserCanReadProject, RateLimiterMiddleware.rateLimit({ - endpointName: "index-all-project-references" - maxRequests: 30 - timeInterval: 60 - }), ReferencesController.indexAll - - # disable beta program while v2 is in beta - 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 - webRouter.get "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.sudoModePrompt - webRouter.post "/confirm-password", - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit({ - endpointName: "confirm-password" - maxRequests: 10 - timeInterval: 60 - }), - SudoModeController.submitPassword - - # New "api" endpoints. Started as a way for v1 to call over to v2 (for - # long-term features, as opposed to the nominally temporary ones in the - # overleaf-integration module), but may expand beyond that role. - publicApiRouter.post '/api/clsi/compile/:submission_id', AuthenticationController.httpAuth, CompileController.compileSubmission - publicApiRouter.get /^\/api\/clsi\/compile\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, - ((req, res, next) -> - params = - "submission_id": req.params[0] - "build_id": req.params[1] - "file": req.params[2] - req.params = params - next() - ), - AuthenticationController.httpAuth, - CompileController.getFileFromClsiWithoutUser - publicApiRouter.post '/api/institutions/confirm_university_domain', RateLimiterMiddleware.rateLimit({ - endpointName: 'confirm-university-domain', - maxRequests: 1, - timeInterval: 60 - }), AuthenticationController.httpAuth, InstitutionsController.confirmDomain - - webRouter.get '/chrome', (req, res, next) -> - # Match v1 behaviour - this is used for a Chrome web app - if AuthenticationController.isUserLoggedIn(req) - res.redirect('/project') - else - res.redirect('/register') - - #Admin Stuff - webRouter.get '/admin', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.index - webRouter.get '/admin/user', AuthorizationMiddleware.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon - webRouter.get '/admin/register', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.registerNewUser - webRouter.post '/admin/register', AuthorizationMiddleware.ensureUserIsSiteAdmin, UserController.register - webRouter.post '/admin/closeEditor', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.closeEditor - webRouter.post '/admin/dissconectAllUsers', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.dissconectAllUsers - webRouter.post '/admin/syncUserToSubscription', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.syncUserToSubscription - webRouter.post '/admin/flushProjectToTpds', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.flushProjectToTpds - webRouter.post '/admin/pollDropboxForUser', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.pollDropboxForUser - webRouter.post '/admin/messages', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.createMessage - webRouter.post '/admin/messages/clear', AuthorizationMiddleware.ensureUserIsSiteAdmin, AdminController.clearMessages - - privateApiRouter.post '/disconnectAllUsers', AdminController.dissconectAllUsers - - privateApiRouter.get '/perfTest', (req,res)-> - res.send("hello") - - publicApiRouter.get '/status', (req,res)-> - res.send("web sharelatex is alive (web)") - privateApiRouter.get '/status', (req,res)-> - res.send("web sharelatex is alive (api)") - - webRouter.get '/dev/csrf', (req, res) -> - res.send res.locals.csrfToken - - publicApiRouter.get '/health_check', HealthCheckController.check - privateApiRouter.get '/health_check', HealthCheckController.check - - publicApiRouter.get '/health_check/redis', HealthCheckController.checkRedis - privateApiRouter.get '/health_check/redis', HealthCheckController.checkRedis - - publicApiRouter.get '/health_check/mongo', HealthCheckController.checkMongo - privateApiRouter.get '/health_check/mongo', HealthCheckController.checkMongo - - webRouter.get "/status/compiler/:Project_id", AuthorizationMiddleware.ensureUserCanReadProject, (req, res) -> - project_id = req.params.Project_id - sendRes = _.once (statusCode, message)-> - res.status statusCode - res.send message - ClsiCookieManager.clearServerId project_id # force every compile to a new server - # set a timeout - handler = setTimeout (() -> - sendRes 500, "Compiler timed out" - handler = null - ), 10000 - # use a valid user id for testing - test_user_id = "123456789012345678901234" - # run the compile - CompileManager.compile project_id, test_user_id, {}, (error, status) -> - clearTimeout handler if handler? - if error? - sendRes 500, "Compiler returned error #{error.message}" - else if status is "success" - sendRes 200, "Compiler returned in less than 10 seconds" - else - sendRes 500, "Compiler returned failure #{status}" - - webRouter.get "/no-cache", (req, res, next)-> - res.header("Cache-Control", "max-age=0") - res.sendStatus(404) - - webRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) - webRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") - webRouter.get '/oops-mongo', (req, res, next) -> - require("./models/Project").Project.findOne {}, () -> - throw new Error("Test error") - - privateApiRouter.get '/opps-small', (req, res, next)-> - logger.err "test error occured" - res.send() - - webRouter.post '/error/client', (req, res, next) -> - logger.warn err: req.body.error, meta: req.body.meta, "client side error" - metrics.inc("client-side-error") - res.sendStatus(204) - - - webRouter.get '/read/:read_only_token([a-z]+)', - RateLimiterMiddleware.rateLimit({ - endpointName: 'read-only-token', - maxRequests: 15, - timeInterval: 60 - }), - TokenAccessController.readOnlyToken - - webRouter.get '/:read_and_write_token([0-9]+[a-z]+)', - RateLimiterMiddleware.rateLimit({ - endpointName: 'read-and-write-token', - maxRequests: 15, - timeInterval: 60 - }), - TokenAccessController.readAndWriteToken - - webRouter.get '*', ErrorController.notFound diff --git a/services/web/app/src/Features/Analytics/AnalyticsController.js b/services/web/app/src/Features/Analytics/AnalyticsController.js new file mode 100644 index 0000000000..af2fe87e7d --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsController.js @@ -0,0 +1,86 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let AnalyticsController +const AnalyticsManager = require('./AnalyticsManager') +const Errors = require('../Errors/Errors') +const AuthenticationController = require('../Authentication/AuthenticationController') +const InstitutionsAPI = require('../Institutions/InstitutionsAPI') +const GeoIpLookup = require('../../infrastructure/GeoIpLookup') + +module.exports = AnalyticsController = { + updateEditingSession(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const { projectId } = req.params + let countryCode = null + + if (userId != null) { + return GeoIpLookup.getDetails(req.ip, function(err, geoDetails) { + if ( + (geoDetails != null ? geoDetails.country_code : undefined) != null && + geoDetails.country_code !== '' + ) { + countryCode = geoDetails.country_code + } + return AnalyticsManager.updateEditingSession( + userId, + projectId, + countryCode, + error => respondWith(error, res, next) + ) + }) + } else { + return res.send(204) + } + }, + + recordEvent(req, res, next) { + const user_id = + AuthenticationController.getLoggedInUserId(req) || req.sessionID + return AnalyticsManager.recordEvent( + user_id, + req.params.event, + req.body, + error => respondWith(error, res, next) + ) + }, + + licences(req, res, next) { + const { resource_id, start_date, end_date, lag } = req.query + return InstitutionsAPI.getInstitutionLicences( + resource_id, + start_date, + end_date, + lag, + function(error, licences) { + if (error != null) { + return next(error) + } else { + return res.send(licences) + } + } + ) + } +} + +var respondWith = function(error, res, next) { + if (error instanceof Errors.ServiceNotConfiguredError) { + // ignore, no-op + return res.send(204) + } else if (error != null) { + return next(error) + } else { + return res.send(204) + } +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsManager.js b/services/web/app/src/Features/Analytics/AnalyticsManager.js new file mode 100644 index 0000000000..e076db0d2b --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsManager.js @@ -0,0 +1,177 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const request = require('requestretry') +const Errors = require('../Errors/Errors') + +const makeFaultTolerantRequest = function(userId, options, callback) { + if ( + userId + '' === + (settings.smokeTest != null ? settings.smokeTest.userId : undefined) + '' + ) { + return callback() + } + + options = Object.assign(options, { + delayStrategy: exponentialBackoffStrategy(), + timeout: 30000 + }) + + if (settings.overleaf != null) { + options.qs = Object.assign({}, options.qs, { fromV2: 1 }) + } + + makeRequest(options, function(err) { + if (err != null) { + return logger.err({ err }, 'Request to analytics failed') + } + }) + + return callback() // Do not wait for all the attempts +} + +var makeRequest = function(opts, callback) { + if ( + __guard__( + settings.apis != null ? settings.apis.analytics : undefined, + x => x.url + ) != null + ) { + const urlPath = opts.url + opts.url = `${settings.apis.analytics.url}${urlPath}` + return request(opts, callback) + } else { + return callback( + new Errors.ServiceNotConfiguredError('Analytics service not configured') + ) + } +} + +// Set an exponential backoff to retry calls to analytics. First retry will +// happen after 4s, then 8, 16, 32, 64... +var exponentialBackoffStrategy = function() { + let attempts = 1 // This won't be called until there has been 1 failure + + return function() { + attempts += 1 + return exponentialBackoffDelay(attempts) + } +} + +var exponentialBackoffDelay = function(attempts) { + const delay = Math.pow(2, attempts) * 1000 + + logger.warn( + 'Error comunicating with the analytics service. ' + + `Will try again attempt ${attempts} in ${delay}ms` + ) + + return delay +} + +module.exports = { + identifyUser(user_id, old_user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + const opts = { + body: { + old_user_id + }, + json: true, + method: 'POST', + timeout: 1000, + url: `/user/${user_id}/identify` + } + return makeRequest(opts, callback) + }, + + recordEvent(user_id, event, segmentation, callback) { + if (segmentation == null) { + segmentation = {} + } + if (callback == null) { + callback = function(error) {} + } + const opts = { + body: { + event, + segmentation + }, + json: true, + method: 'POST', + url: `/user/${user_id}/event`, + maxAttempts: 7 // Give up after ~ 8min + } + + return makeFaultTolerantRequest(user_id, opts, callback) + }, + + updateEditingSession(userId, projectId, countryCode, callback) { + if (callback == null) { + callback = function(error) {} + } + const query = { + userId, + projectId + } + + if (countryCode) { + query.countryCode = countryCode + } + + const opts = { + method: 'PUT', + url: '/editingSession', + qs: query, + maxAttempts: 6 // Give up after ~ 4min + } + + return makeFaultTolerantRequest(userId, opts, callback) + }, + + getLastOccurrence(user_id, event, callback) { + if (callback == null) { + callback = function(error) {} + } + const opts = { + body: { + event + }, + json: true, + method: 'POST', + timeout: 1000, + url: `/user/${user_id}/event/last_occurrence` + } + return makeRequest(opts, function(err, response, body) { + if (err != null) { + console.log(response, opts) + logger.err({ user_id, err }, 'error getting last occurance of event') + return callback(err) + } else { + return callback(null, body) + } + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsProxy.js b/services/web/app/src/Features/Analytics/AnalyticsProxy.js new file mode 100644 index 0000000000..9510025a16 --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsProxy.js @@ -0,0 +1,53 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('settings-sharelatex') +const Errors = require('../Errors/Errors') +const httpProxy = require('express-http-proxy') +const URL = require('url') + +module.exports = { + call(basePath) { + const analyticsUrl = __guard__( + __guard__( + settings != null ? settings.apis : undefined, + x1 => x1.analytics + ), + x => x.url + ) + if (analyticsUrl != null) { + return httpProxy(analyticsUrl, { + proxyReqPathResolver(req) { + const requestPath = URL.parse(req.url).path + return `${basePath}${requestPath}` + }, + proxyReqOptDecorator(proxyReqOpts, srcReq) { + proxyReqOpts.headers = {} // unset all headers + return proxyReqOpts + } + }) + } else { + return (req, res, next) => + next( + new Errors.ServiceNotConfiguredError( + 'Analytics service not configured' + ) + ) + } + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsRouter.js b/services/web/app/src/Features/Analytics/AnalyticsRouter.js new file mode 100644 index 0000000000..6ffcf51704 --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsRouter.js @@ -0,0 +1,45 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthenticationController = require('./../Authentication/AuthenticationController') +const AnalyticsController = require('./AnalyticsController') +const AnalyticsProxy = require('./AnalyticsProxy') + +module.exports = { + apply(webRouter, privateApiRouter, publicApiRouter) { + webRouter.post('/event/:event', AnalyticsController.recordEvent) + + webRouter.put( + '/editingSession/:projectId', + AnalyticsController.updateEditingSession + ) + + publicApiRouter.use( + '/analytics/graphs', + AuthenticationController.httpAuth, + AnalyticsProxy.call('/graphs') + ) + + publicApiRouter.use( + '/analytics/recentTeamActivity', + AuthenticationController.httpAuth, + AnalyticsProxy.call('/recentTeamActivity') + ) + + publicApiRouter.use( + '/analytics/recentV1TemplateIdsActivity', + AuthenticationController.httpAuth, + AnalyticsProxy.call('/recentV1TemplateIdsActivity') + ) + + return publicApiRouter.use( + '/analytics/uniExternalCollaboration', + AuthenticationController.httpAuth, + AnalyticsProxy.call('/uniExternalCollaboration') + ) + } +} diff --git a/services/web/app/src/Features/Announcements/AnnouncementsController.js b/services/web/app/src/Features/Announcements/AnnouncementsController.js new file mode 100644 index 0000000000..d515c6620e --- /dev/null +++ b/services/web/app/src/Features/Announcements/AnnouncementsController.js @@ -0,0 +1,59 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AnnouncementsHandler = require('./AnnouncementsHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') + +module.exports = { + getUndreadAnnouncements(req, res, next) { + if ( + __guard__( + __guard__( + settings != null ? settings.apis : undefined, + x1 => x1.analytics + ), + x => x.url + ) == null || + settings.apis.blog.url == null + ) { + return res.json([]) + } + + const user = AuthenticationController.getSessionUser(req) + logger.log( + { user_id: user != null ? user._id : undefined }, + 'getting unread announcements' + ) + return AnnouncementsHandler.getUnreadAnnouncements(user, function( + err, + announcements + ) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'unable to get unread announcements' + ) + return next(err) + } else { + return res.json(announcements) + } + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Announcements/AnnouncementsHandler.js b/services/web/app/src/Features/Announcements/AnnouncementsHandler.js new file mode 100644 index 0000000000..36b24e2f85 --- /dev/null +++ b/services/web/app/src/Features/Announcements/AnnouncementsHandler.js @@ -0,0 +1,135 @@ +/* eslint-disable + handle-callback-err, + max-len, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let AnnouncementsHandler +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const BlogHandler = require('../Blog/BlogHandler') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const async = require('async') +const _ = require('lodash') + +module.exports = AnnouncementsHandler = { + _domainSpecificAnnouncements(email) { + const domainSpecific = _.filter( + settings != null ? settings.domainAnnouncements : undefined, + function(domainAnnouncment) { + const matches = _.filter( + domainAnnouncment.domains, + domain => email.indexOf(domain) !== -1 + ) + return matches.length > 0 && domainAnnouncment.id != null + } + ) + return domainSpecific || [] + }, + + getUnreadAnnouncements(user, callback) { + if (callback == null) { + callback = function(err, announcements) {} + } + if (user == null && user._id == null) { + return callback('user not supplied') + } + + const timestamp = user._id.toString().substring(0, 8) + const userSignupDate = new Date(parseInt(timestamp, 16) * 1000) + + return async.parallel( + { + lastEvent(cb) { + return AnalyticsManager.getLastOccurrence( + user._id, + 'announcement-alert-dismissed', + cb + ) + }, + announcements(cb) { + return BlogHandler.getLatestAnnouncements(cb) + } + }, + function(err, results) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'error getting unread announcements' + ) + return callback(err) + } + + let domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements( + user != null ? user.email : undefined + ) + + domainSpecific = _.map(domainSpecific, function(domainAnnouncment) { + try { + domainAnnouncment.date = new Date(domainAnnouncment.date) + return domainAnnouncment + } catch (e) { + return callback(e) + } + }) + + let { announcements } = results + announcements = _.union(announcements, domainSpecific) + announcements = _.sortBy(announcements, 'date').reverse() + + const lastSeenBlogId = __guard__( + __guard__( + results != null ? results.lastEvent : undefined, + x1 => x1.segmentation + ), + x => x.blogPostId + ) + + const announcementIndex = _.findIndex( + announcements, + announcement => announcement.id === lastSeenBlogId + ) + + announcements = _.map(announcements, function(announcement, index) { + let read + if (announcement.date < userSignupDate) { + read = true + } else if (announcementIndex === -1) { + read = false + } else if (index >= announcementIndex) { + read = true + } else { + read = false + } + announcement.read = read + return announcement + }) + + logger.log( + { + announcementsLength: + announcements != null ? announcements.length : undefined, + user_id: user != null ? user._id : undefined + }, + 'returning announcements' + ) + + return callback(null, announcements) + } + ) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.js b/services/web/app/src/Features/Authentication/AuthenticationController.js new file mode 100644 index 0000000000..0a89cea909 --- /dev/null +++ b/services/web/app/src/Features/Authentication/AuthenticationController.js @@ -0,0 +1,578 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let AuthenticationController +const AuthenticationManager = require('./AuthenticationManager') +const LoginRateLimiter = require('../Security/LoginRateLimiter') +const UserUpdater = require('../User/UserUpdater') +const Metrics = require('metrics-sharelatex') +const logger = require('logger-sharelatex') +const querystring = require('querystring') +const Url = require('url') +const Settings = require('settings-sharelatex') +const basicAuth = require('basic-auth-connect') +const UserHandler = require('../User/UserHandler') +const UserSessionsManager = require('../User/UserSessionsManager') +const Analytics = require('../Analytics/AnalyticsManager') +const passport = require('passport') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const SudoModeHandler = require('../SudoMode/SudoModeHandler') +const V1Api = require('../V1/V1Api') +const { User } = require('../../models/User') +const { URL } = require('url') + +module.exports = AuthenticationController = { + serializeUser(user, callback) { + const lightUser = { + _id: user._id, + first_name: user.first_name, + last_name: user.last_name, + isAdmin: user.isAdmin, + staffAccess: user.staffAccess, + email: user.email, + referal_id: user.referal_id, + session_created: new Date().toISOString(), + ip_address: user._login_req_ip, + must_reconfirm: user.must_reconfirm, + v1_id: user.overleaf != null ? user.overleaf.id : undefined + } + return callback(null, lightUser) + }, + + deserializeUser(user, cb) { + return cb(null, user) + }, + + afterLoginSessionSetup(req, user, callback) { + if (callback == null) { + callback = function(err) {} + } + return req.login(user, function(err) { + if (err != null) { + logger.err({ user_id: user._id, err }, 'error from req.login') + return callback(err) + } + // Regenerate the session to get a new sessionID (cookie value) to + // protect against session fixation attacks + const oldSession = req.session + return req.session.destroy(function(err) { + if (err != null) { + logger.err( + { user_id: user._id, err }, + 'error when trying to destroy old session' + ) + return callback(err) + } + req.sessionStore.generate(req) + for (let key in oldSession) { + const value = oldSession[key] + if (key !== '__tmp') { + req.session[key] = value + } + } + // copy to the old `session.user` location, for backward-comptability + req.session.user = req.session.passport.user + return req.session.save(function(err) { + if (err != null) { + logger.err( + { user_id: user._id }, + 'error saving regenerated session after login' + ) + return callback(err) + } + UserSessionsManager.trackSession(user, req.sessionID, function() {}) + return callback(null) + }) + }) + }) + }, + + passportLogin(req, res, next) { + // This function is middleware which wraps the passport.authenticate middleware, + // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure, + // and send a `{redir: ""}` response on success + return passport.authenticate('local', function(err, user, info) { + if (err != null) { + return next(err) + } + if (user) { + // `user` is either a user object or false + return AuthenticationController.finishLogin(user, req, res, next) + } else { + if (info.redir != null) { + return res.json({ redir: info.redir }) + } else { + return res.json({ message: info }) + } + } + })(req, res, next) + }, + + finishLogin(user, req, res, next) { + if (user === false) { + return res.redirect('/login') + } // OAuth2 'state' mismatch + if (user.must_reconfirm) { + return AuthenticationController._redirectToReconfirmPage(req, res, user) + } else { + const redir = + AuthenticationController._getRedirectFromSession(req) || '/project' + AuthenticationController._loginAsyncHandlers(req, user) + return AuthenticationController.afterLoginSessionSetup( + req, + user, + function(err) { + if (err != null) { + return next(err) + } + return SudoModeHandler.activateSudoMode(user._id, function(err) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'Error activating Sudo Mode on login, continuing' + ) + } + AuthenticationController._clearRedirectFromSession(req) + if ( + __guard__( + req.headers != null ? req.headers['accept'] : undefined, + x => x.match(/^application\/json.*$/) + ) + ) { + return res.json({ redir }) + } else { + return res.redirect(redir) + } + }) + } + ) + } + }, + + doPassportLogin(req, username, password, done) { + const email = username.toLowerCase() + const Modules = require('../../infrastructure/Modules') + return Modules.hooks.fire('preDoPassportLogin', req, email, function( + err, + infoList + ) { + if (err != null) { + return next(err) + } + const info = infoList.find(i => i != null) + if (info != null) { + return done(null, false, info) + } + return LoginRateLimiter.processLoginRequest(email, function( + err, + isAllowed + ) { + if (err != null) { + return done(err) + } + if (!isAllowed) { + logger.log({ email }, 'too many login requests') + return done(null, null, { + text: req.i18n.translate('to_many_login_requests_2_mins'), + type: 'error' + }) + } + return AuthenticationManager.authenticate({ email }, password, function( + error, + user + ) { + if (error != null) { + return done(error) + } + if (user != null) { + // async actions + return done(null, user) + } else { + AuthenticationController._recordFailedLogin() + logger.log({ email }, 'failed log in') + return done(null, false, { + text: req.i18n.translate('email_or_password_wrong_try_again'), + type: 'error' + }) + } + }) + }) + }) + }, + + _loginAsyncHandlers(req, user) { + UserHandler.setupLoginData(user, function() {}) + LoginRateLimiter.recordSuccessfulLogin(user.email) + AuthenticationController._recordSuccessfulLogin(user._id) + AuthenticationController.ipMatchCheck(req, user) + Analytics.recordEvent(user._id, 'user-logged-in', { ip: req.ip }) + Analytics.identifyUser(user._id, req.sessionID) + logger.log( + { email: user.email, user_id: user._id.toString() }, + 'successful log in' + ) + req.session.justLoggedIn = true + // capture the request ip for use when creating the session + return (user._login_req_ip = req.ip) + }, + + ipMatchCheck(req, user) { + if (req.ip !== user.lastLoginIp) { + NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip) + } + return UserUpdater.updateUser(user._id.toString(), { + $set: { lastLoginIp: req.ip } + }) + }, + + setInSessionUser(req, props) { + return (() => { + const result = [] + for (let key in props) { + const value = props[key] + if ( + __guard__( + __guard__(req != null ? req.session : undefined, x1 => x1.passport), + x => x.user + ) != null + ) { + req.session.passport.user[key] = value + } + if ( + __guard__(req != null ? req.session : undefined, x2 => x2.user) != + null + ) { + result.push((req.session.user[key] = value)) + } else { + result.push(undefined) + } + } + return result + })() + }, + + isUserLoggedIn(req) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return ![null, undefined, false].includes(user_id) + }, + + // TODO: perhaps should produce an error if the current user is not present + getLoggedInUserId(req) { + const user = AuthenticationController.getSessionUser(req) + if (user) { + return user._id + } else { + return null + } + }, + + getLoggedInUserV1Id(req) { + const user = AuthenticationController.getSessionUser(req) + if ((user != null ? user.v1_id : undefined) != null) { + return user.v1_id + } else { + return null + } + }, + + getSessionUser(req) { + if (__guard__(req != null ? req.session : undefined, x => x.user) != null) { + return req.session.user + } else if ( + __guard__( + __guard__(req != null ? req.session : undefined, x2 => x2.passport), + x1 => x1.user + ) + ) { + return req.session.passport.user + } else { + return null + } + }, + + requireLogin() { + const doRequest = function(req, res, next) { + if (next == null) { + next = function(error) {} + } + if (!AuthenticationController.isUserLoggedIn(req)) { + return AuthenticationController._redirectToLoginOrRegisterPage(req, res) + } else { + req.user = AuthenticationController.getSessionUser(req) + return next() + } + } + + return doRequest + }, + + // access tokens might be associated with user stubs if the user is + // not yet migrated to v2. if api can work with user stubs then set + // allowUserStub true when adding middleware to route. + requireOauth(allowUserStub) { + // require this here because module may not be included in some versions + if (allowUserStub == null) { + allowUserStub = false + } + const Oauth2Server = require('../../../../modules/oauth2-server/app/src/Oauth2Server') + return function(req, res, next) { + if (next == null) { + next = function(error) {} + } + const request = new Oauth2Server.Request(req) + const response = new Oauth2Server.Response(res) + return Oauth2Server.server.authenticate(request, response, {}, function( + err, + token + ) { + if (err != null) { + // use a 401 status code for malformed header for git-bridge + if ( + err.code === 400 && + err.message === 'Invalid request: malformed authorization header' + ) { + err.code = 401 + } + // fall back to v1 on invalid token + if (err.code === 401) { + return AuthenticationController._requireOauthV1Fallback( + req, + res, + next + ) + } + // send all other errors + return res + .status(err.code) + .json({ error: err.name, error_description: err.message }) + } + if (token.user.constructor.modelName === 'UserStub' && !allowUserStub) { + return res.sendStatus(401) + } + req.oauth = { access_token: token.accessToken } + req.oauth_token = token + req.oauth_user = token.user + return next() + }) + } + }, + + _requireOauthV1Fallback(req, res, next) { + if (req.token == null) { + return res.sendStatus(401) + } + const options = { + expectedStatusCodes: [401], + json: { + token: req.token + }, + method: 'POST', + uri: '/api/v1/sharelatex/oauth_authorize' + } + return V1Api.request(options, function(error, response, body) { + if (error != null) { + return next(error) + } + if (!__guard__(body != null ? body.user_profile : undefined, x => x.id)) { + return res.status(401).json({ error: 'invalid_token' }) + } + return User.findOne({ 'overleaf.id': body.user_profile.id }, function( + error, + user + ) { + if (error != null) { + return next(error) + } + if (user == null) { + return res.status(401).json({ error: 'invalid_token' }) + } + req.oauth = { access_token: body.access_token } + req.oauth_user = user + return next() + }) + }) + }, + + _globalLoginWhitelist: [], + addEndpointToLoginWhitelist(endpoint) { + return AuthenticationController._globalLoginWhitelist.push(endpoint) + }, + + requireGlobalLogin(req, res, next) { + if ( + Array.from(AuthenticationController._globalLoginWhitelist).includes( + req._parsedUrl.pathname + ) + ) { + return next() + } + + if (req.headers['authorization'] != null) { + return AuthenticationController.httpAuth(req, res, next) + } else if (AuthenticationController.isUserLoggedIn(req)) { + return next() + } else { + logger.log( + { url: req.url }, + 'user trying to access endpoint not in global whitelist' + ) + AuthenticationController.setRedirectInSession(req) + return res.redirect('/login') + } + }, + + httpAuth: basicAuth(function(user, pass) { + const isValid = Settings.httpAuthUsers[user] === pass + if (!isValid) { + logger.err({ user, pass }, 'invalid login details') + } + return isValid + }), + + setRedirectInSession(req, value) { + if (value == null) { + value = + Object.keys(req.query).length > 0 + ? `${req.path}?${querystring.stringify(req.query)}` + : `${req.path}` + } + if ( + req.session != null && + !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) && + !/^.*\.(png|jpeg|svg)$/.test(value) + ) { + const safePath = AuthenticationController._getSafeRedirectPath(value) + return (req.session.postLoginRedirect = safePath) + } + }, + + _redirectToLoginOrRegisterPage(req, res) { + if ( + req.query.zipUrl != null || + req.query.project_name != null || + req.path === '/user/subscription/new' + ) { + return AuthenticationController._redirectToRegisterPage(req, res) + } else { + return AuthenticationController._redirectToLoginPage(req, res) + } + }, + + _redirectToLoginPage(req, res) { + logger.log( + { url: req.url }, + 'user not logged in so redirecting to login page' + ) + AuthenticationController.setRedirectInSession(req) + const url = `/login?${querystring.stringify(req.query)}` + res.redirect(url) + return Metrics.inc('security.login-redirect') + }, + + _redirectToReconfirmPage(req, res, user) { + logger.log( + { url: req.url }, + 'user needs to reconfirm so redirecting to reconfirm page' + ) + req.session.reconfirm_email = user != null ? user.email : undefined + const redir = '/user/reconfirm' + if ( + __guard__(req.headers != null ? req.headers['accept'] : undefined, x => + x.match(/^application\/json.*$/) + ) + ) { + return res.json({ redir }) + } else { + return res.redirect(redir) + } + }, + + _redirectToRegisterPage(req, res) { + logger.log( + { url: req.url }, + 'user not logged in so redirecting to register page' + ) + AuthenticationController.setRedirectInSession(req) + const url = `/register?${querystring.stringify(req.query)}` + res.redirect(url) + return Metrics.inc('security.login-redirect') + }, + + _recordSuccessfulLogin(user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return UserUpdater.updateUser( + user_id.toString(), + { + $set: { lastLoggedIn: new Date() }, + $inc: { loginCount: 1 } + }, + function(error) { + if (error != null) { + callback(error) + } + Metrics.inc('user.login.success') + return callback() + } + ) + }, + + _recordFailedLogin(callback) { + if (callback == null) { + callback = function(error) {} + } + Metrics.inc('user.login.failed') + return callback() + }, + + _getRedirectFromSession(req) { + let safePath + const value = __guard__( + req != null ? req.session : undefined, + x => x.postLoginRedirect + ) + if (value) { + safePath = AuthenticationController._getSafeRedirectPath(value) + } + return safePath || null + }, + + _clearRedirectFromSession(req) { + if (req.session != null) { + return delete req.session.postLoginRedirect + } + }, + + _getSafeRedirectPath(value) { + const baseURL = Settings.siteUrl // base URL is required to construct URL from path + const url = new URL(value, baseURL) + let safePath = `${url.pathname}${url.search}${url.hash}` + if (safePath === '/') { + safePath = undefined + } + return safePath + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Authentication/AuthenticationManager.js b/services/web/app/src/Features/Authentication/AuthenticationManager.js new file mode 100644 index 0000000000..af742aeba4 --- /dev/null +++ b/services/web/app/src/Features/Authentication/AuthenticationManager.js @@ -0,0 +1,320 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS202: Simplify dynamic range loops + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let AuthenticationManager +const Settings = require('settings-sharelatex') +const { User } = require('../../models/User') +const { db, ObjectId } = require('../../infrastructure/mongojs') +const crypto = require('crypto') +const bcrypt = require('bcrypt') +const EmailHelper = require('../Helpers/EmailHelper') +const Errors = require('../Errors/Errors') +const UserGetter = require('../User/UserGetter') +const V1Handler = require('../V1/V1Handler') + +const BCRYPT_ROUNDS = + __guard__( + Settings != null ? Settings.security : undefined, + x => x.bcryptRounds + ) || 12 + +const _checkWriteResult = function(result, callback) { + // for MongoDB + if (callback == null) { + callback = function(error, updated) {} + } + if (result && result.nModified === 1) { + return callback(null, true) + } else { + return callback(null, false) + } +} + +module.exports = AuthenticationManager = { + authenticate(query, password, callback) { + // Using Mongoose for legacy reasons here. The returned User instance + // gets serialized into the session and there may be subtle differences + // between the user returned by Mongoose vs mongojs (such as default values) + if (callback == null) { + callback = function(error, user) {} + } + return User.findOne(query, (error, user) => { + if (error != null) { + return callback(error) + } + if (user != null) { + if (user.hashedPassword != null) { + return bcrypt.compare(password, user.hashedPassword, function( + error, + match + ) { + if (error != null) { + return callback(error) + } + if (match) { + return AuthenticationManager.checkRounds( + user, + user.hashedPassword, + password, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, user) + } + ) + } else { + return callback(null, null) + } + }) + } else { + return callback(null, null) + } + } else { + return callback(null, null) + } + }) + }, + + validateEmail(email) { + const parsed = EmailHelper.parseEmail(email) + if (parsed == null) { + return { message: 'email not valid' } + } + return null + }, + + // validates a password based on a similar set of rules to `complexPassword.js` on the frontend + // note that `passfield.js` enforces more rules than this, but these are the most commonly set. + // returns null on success, or an error string. + validatePassword(password) { + if (password == null) { + return { message: 'password not set' } + } + + const allowAnyChars = + (Settings.passwordStrengthOptions != null + ? Settings.passwordStrengthOptions.allowAnyChars + : undefined) === true + const min = + __guard__( + Settings.passwordStrengthOptions != null + ? Settings.passwordStrengthOptions.length + : undefined, + x1 => x1.min + ) || 6 + let max = + __guard__( + Settings.passwordStrengthOptions != null + ? Settings.passwordStrengthOptions.length + : undefined, + x2 => x2.max + ) || 72 + + // we don't support passwords > 72 characters in length, because bcrypt truncates them + if (max > 72) { + max = 72 + } + + if (!(password.length >= min)) { + return { message: 'password is too short' } + } + if (!(password.length <= max)) { + return { message: 'password is too long' } + } + if ( + !allowAnyChars && + !AuthenticationManager._passwordCharactersAreValid(password) + ) { + return { message: 'password contains an invalid character' } + } + return null + }, + + setUserPassword(user_id, password, callback) { + if (callback == null) { + callback = function(error, changed) {} + } + const validation = this.validatePassword(password) + if (validation != null) { + return callback(validation.message) + } + + return UserGetter.getUser(user_id, { email: 1, overleaf: 1 }, function( + error, + user + ) { + if (error != null) { + return callback(error) + } + const v1IdExists = + (user.overleaf != null ? user.overleaf.id : undefined) != null + if (v1IdExists && Settings.overleaf != null) { + // v2 user in v2 + // v2 user in v2, change password in v1 + return AuthenticationManager.setUserPasswordInV1( + user.overleaf.id, + password, + callback + ) + } else if (v1IdExists && Settings.overleaf == null) { + // v2 user in SL + return callback(new Errors.NotInV2Error('Password Reset Attempt')) + } else if (!v1IdExists && Settings.overleaf == null) { + // SL user in SL, change password in SL + return AuthenticationManager.setUserPasswordInV2( + user_id, + password, + callback + ) + } else if (!v1IdExists && Settings.overleaf != null) { + // SL user in v2, should not happen + return callback(new Errors.SLInV2Error('Password Reset Attempt')) + } else { + return callback(new Error('Password Reset Attempt Failed')) + } + }) + }, + + checkRounds(user, hashedPassword, password, callback) { + // Temporarily disable this function, TODO: re-enable this + if (callback == null) { + callback = function(error) {} + } + if ( + __guard__( + Settings != null ? Settings.security : undefined, + x1 => x1.disableBcryptRoundsUpgrades + ) + ) { + return callback() + } + // check current number of rounds and rehash if necessary + const currentRounds = bcrypt.getRounds(hashedPassword) + if (currentRounds < BCRYPT_ROUNDS) { + return AuthenticationManager.setUserPassword(user._id, password, callback) + } else { + return callback() + } + }, + + setUserPasswordInV2(user_id, password, callback) { + const validation = this.validatePassword(password) + if (validation != null) { + return callback(validation.message) + } + const minorVersion = 'a' + return bcrypt.genSalt(BCRYPT_ROUNDS, minorVersion, function(error, salt) { + if (error != null) { + return callback(error) + } + return bcrypt.hash(password, salt, function(error, hash) { + if (error != null) { + return callback(error) + } + return db.users.update( + { + _id: ObjectId(user_id.toString()) + }, + { + $set: { + hashedPassword: hash + }, + $unset: { + password: true + } + }, + function(updateError, result) { + if (updateError != null) { + return callback(updateError) + } + return _checkWriteResult(result, callback) + } + ) + }) + }) + }, + + setUserPasswordInV1(v1_user_id, password, callback) { + const validation = this.validatePassword(password) + if (validation != null) { + return callback(validation.message) + } + + return V1Handler.doPasswordReset(v1_user_id, password, function( + error, + reset + ) { + if (error != null) { + return callback(error) + } + return callback(error, reset) + }) + }, + + _passwordCharactersAreValid(password) { + const digits = + __guard__( + Settings.passwordStrengthOptions != null + ? Settings.passwordStrengthOptions.chars + : undefined, + x1 => x1.digits + ) || '1234567890' + const letters = + __guard__( + Settings.passwordStrengthOptions != null + ? Settings.passwordStrengthOptions.chars + : undefined, + x2 => x2.letters + ) || 'abcdefghijklmnopqrstuvwxyz' + const letters_up = + __guard__( + Settings.passwordStrengthOptions != null + ? Settings.passwordStrengthOptions.chars + : undefined, + x3 => x3.letters_up + ) || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + const symbols = + __guard__( + Settings.passwordStrengthOptions != null + ? Settings.passwordStrengthOptions.chars + : undefined, + x4 => x4.symbols + ) || '@#$%^&*()-_=+[]{};:<>/?!£€.,' + + for ( + let charIndex = 0, end = password.length - 1, asc = end >= 0; + asc ? charIndex <= end : charIndex >= end; + asc ? charIndex++ : charIndex-- + ) { + if ( + !(digits.indexOf(password[charIndex]) > -1) && + !(letters.indexOf(password[charIndex]) > -1) && + !(letters_up.indexOf(password[charIndex]) > -1) && + !(symbols.indexOf(password[charIndex]) > -1) + ) { + return false + } + } + return true + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Authorization/AuthorizationManager.js b/services/web/app/src/Features/Authorization/AuthorizationManager.js new file mode 100644 index 0000000000..5c101c8aeb --- /dev/null +++ b/services/web/app/src/Features/Authorization/AuthorizationManager.js @@ -0,0 +1,289 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let AuthorizationManager +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const ProjectGetter = require('../Project/ProjectGetter') +const { User } = require('../../models/User') +const PrivilegeLevels = require('./PrivilegeLevels') +const PublicAccessLevels = require('./PublicAccessLevels') +const Errors = require('../Errors/Errors') +const { ObjectId } = require('mongojs') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') + +module.exports = AuthorizationManager = { + getPublicAccessLevel(project_id, callback) { + if (callback == null) { + callback = function(err, level) {} + } + if (!ObjectId.isValid(project_id)) { + return callback(new Error('invalid project id')) + } + // Note, the Project property in the DB is `publicAccesLevel`, without the second `s` + return ProjectGetter.getProject( + project_id, + { publicAccesLevel: 1 }, + function(error, project) { + if (error != null) { + return callback(error) + } + if (project == null) { + return callback( + new Errors.NotFoundError(`no project found with id ${project_id}`) + ) + } + return callback(null, project.publicAccesLevel) + } + ) + }, + + // Get the privilege level that the user has for the project + // Returns: + // * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has + // access. false if the user does not have access + // * becausePublic: true if the access level is only because the project is public. + // * becauseSiteAdmin: true if access level is only because user is admin + getPrivilegeLevelForProject(user_id, project_id, token, callback) { + if (callback == null) { + callback = function( + error, + privilegeLevel, + becausePublic, + becauseSiteAdmin + ) {} + } + if (user_id == null) { + // User is Anonymous, Try Token-based access + return AuthorizationManager.getPublicAccessLevel(project_id, function( + err, + publicAccessLevel + ) { + if (err != null) { + return callback(err) + } + if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) { + // Anonymous users can have read-only access to token-based projects, + // while read-write access must be logged in, + // unless the `enableAnonymousReadAndWriteSharing` setting is enabled + return TokenAccessHandler.isValidToken(project_id, token, function( + err, + isValidReadAndWrite, + isValidReadOnly + ) { + if (err != null) { + return callback(err) + } + if (isValidReadOnly) { + // Grant anonymous user read-only access + return callback(null, PrivilegeLevels.READ_ONLY, false, false) + } else if ( + isValidReadAndWrite && + TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED + ) { + // Grant anonymous user read-and-write access + return callback( + null, + PrivilegeLevels.READ_AND_WRITE, + false, + false + ) + } else { + // Deny anonymous access + return callback(null, PrivilegeLevels.NONE, false, false) + } + }) + } else if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { + // Legacy public read-only access for anonymous user + return callback(null, PrivilegeLevels.READ_ONLY, true, false) + } else if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) { + // Legacy public read-write access for anonymous user + return callback(null, PrivilegeLevels.READ_AND_WRITE, true, false) + } else { + // Deny anonymous user access + return callback(null, PrivilegeLevels.NONE, false, false) + } + }) + } else { + // User is present, get their privilege level from database + return CollaboratorsHandler.getMemberIdPrivilegeLevel( + user_id, + project_id, + function(error, privilegeLevel) { + if (error != null) { + return callback(error) + } + if ( + privilegeLevel != null && + privilegeLevel !== PrivilegeLevels.NONE + ) { + // The user has direct access + return callback(null, privilegeLevel, false, false) + } else { + return AuthorizationManager.isUserSiteAdmin(user_id, function( + error, + isAdmin + ) { + if (error != null) { + return callback(error) + } + if (isAdmin) { + return callback(null, PrivilegeLevels.OWNER, false, true) + } else { + // Legacy public-access system + // User is present (not anonymous), but does not have direct access + return AuthorizationManager.getPublicAccessLevel( + project_id, + function(err, publicAccessLevel) { + if (err != null) { + return callback(err) + } + if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { + return callback( + null, + PrivilegeLevels.READ_ONLY, + true, + false + ) + } else if ( + publicAccessLevel === PublicAccessLevels.READ_AND_WRITE + ) { + return callback( + null, + PrivilegeLevels.READ_AND_WRITE, + true, + false + ) + } else { + return callback(null, PrivilegeLevels.NONE, false, false) + } + } + ) + } + }) + } + } + ) + } + }, + + canUserReadProject(user_id, project_id, token, callback) { + if (callback == null) { + callback = function(error, canRead) {} + } + return AuthorizationManager.getPrivilegeLevelForProject( + user_id, + project_id, + token, + function(error, privilegeLevel) { + if (error != null) { + return callback(error) + } + return callback( + null, + [ + PrivilegeLevels.OWNER, + PrivilegeLevels.READ_AND_WRITE, + PrivilegeLevels.READ_ONLY + ].includes(privilegeLevel) + ) + } + ) + }, + + canUserWriteProjectContent(user_id, project_id, token, callback) { + if (callback == null) { + callback = function(error, canWriteContent) {} + } + return AuthorizationManager.getPrivilegeLevelForProject( + user_id, + project_id, + token, + function(error, privilegeLevel) { + if (error != null) { + return callback(error) + } + return callback( + null, + [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes( + privilegeLevel + ) + ) + } + ) + }, + + canUserWriteProjectSettings(user_id, project_id, token, callback) { + if (callback == null) { + callback = function(error, canWriteSettings) {} + } + return AuthorizationManager.getPrivilegeLevelForProject( + user_id, + project_id, + token, + function(error, privilegeLevel, becausePublic) { + if (error != null) { + return callback(error) + } + if (privilegeLevel === PrivilegeLevels.OWNER) { + return callback(null, true) + } else if ( + privilegeLevel === PrivilegeLevels.READ_AND_WRITE && + !becausePublic + ) { + return callback(null, true) + } else { + return callback(null, false) + } + } + ) + }, + + canUserAdminProject(user_id, project_id, token, callback) { + if (callback == null) { + callback = function(error, canAdmin, becauseSiteAdmin) {} + } + return AuthorizationManager.getPrivilegeLevelForProject( + user_id, + project_id, + token, + function(error, privilegeLevel, becausePublic, becauseSiteAdmin) { + if (error != null) { + return callback(error) + } + return callback( + null, + privilegeLevel === PrivilegeLevels.OWNER, + becauseSiteAdmin + ) + } + ) + }, + + isUserSiteAdmin(user_id, callback) { + if (callback == null) { + callback = function(error, isAdmin) {} + } + if (user_id == null) { + return callback(null, false) + } + return User.findOne({ _id: user_id }, { isAdmin: 1 }, function( + error, + user + ) { + if (error != null) { + return callback(error) + } + return callback(null, (user != null ? user.isAdmin : undefined) === true) + }) + } +} diff --git a/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js b/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js new file mode 100644 index 0000000000..bae9a9d7b0 --- /dev/null +++ b/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js @@ -0,0 +1,298 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let AuthorizationMiddleware +const AuthorizationManager = require('./AuthorizationManager') +const async = require('async') +const logger = require('logger-sharelatex') +const { ObjectId } = require('mongojs') +const Errors = require('../Errors/Errors') +const AuthenticationController = require('../Authentication/AuthenticationController') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') + +module.exports = AuthorizationMiddleware = { + ensureUserCanReadMultipleProjects(req, res, next) { + const project_ids = (req.query.project_ids || '').split(',') + return AuthorizationMiddleware._getUserId(req, function(error, user_id) { + if (error != null) { + return next(error) + } + // Remove the projects we have access to. Note rejectSeries doesn't use + // errors in callbacks + return async.rejectSeries( + project_ids, + function(project_id, cb) { + const token = TokenAccessHandler.getRequestToken(req, project_id) + return AuthorizationManager.canUserReadProject( + user_id, + project_id, + token, + function(error, canRead) { + if (error != null) { + return next(error) + } + return cb(canRead) + } + ) + }, + function(unauthorized_project_ids) { + if (unauthorized_project_ids.length > 0) { + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } else { + return next() + } + } + ) + }) + }, + + ensureUserCanReadProject(req, res, next) { + return AuthorizationMiddleware._getUserAndProjectId(req, function( + error, + user_id, + project_id + ) { + if (error != null) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, project_id) + return AuthorizationManager.canUserReadProject( + user_id, + project_id, + token, + function(error, canRead) { + if (error != null) { + return next(error) + } + if (canRead) { + logger.log( + { user_id, project_id }, + 'allowing user read access to project' + ) + return next() + } else { + logger.log( + { user_id, project_id }, + 'denying user read access to project' + ) + if ( + __guard__( + req.headers != null ? req.headers['accept'] : undefined, + x => x.match(/^application\/json.*$/) + ) + ) { + return res.sendStatus(403) + } else { + return AuthorizationMiddleware.redirectToRestricted( + req, + res, + next + ) + } + } + } + ) + }) + }, + + ensureUserCanWriteProjectSettings(req, res, next) { + return AuthorizationMiddleware._getUserAndProjectId(req, function( + error, + user_id, + project_id + ) { + if (error != null) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, project_id) + return AuthorizationManager.canUserWriteProjectSettings( + user_id, + project_id, + token, + function(error, canWrite) { + if (error != null) { + return next(error) + } + if (canWrite) { + logger.log( + { user_id, project_id }, + 'allowing user write access to project settings' + ) + return next() + } else { + logger.log( + { user_id, project_id }, + 'denying user write access to project settings' + ) + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + } + ) + }) + }, + + ensureUserCanWriteProjectContent(req, res, next) { + return AuthorizationMiddleware._getUserAndProjectId(req, function( + error, + user_id, + project_id + ) { + if (error != null) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, project_id) + return AuthorizationManager.canUserWriteProjectContent( + user_id, + project_id, + token, + function(error, canWrite) { + if (error != null) { + return next(error) + } + if (canWrite) { + logger.log( + { user_id, project_id }, + 'allowing user write access to project content' + ) + return next() + } else { + logger.log( + { user_id, project_id }, + 'denying user write access to project settings' + ) + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + } + ) + }) + }, + + ensureUserCanAdminProject(req, res, next) { + return AuthorizationMiddleware._getUserAndProjectId(req, function( + error, + user_id, + project_id + ) { + if (error != null) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, project_id) + return AuthorizationManager.canUserAdminProject( + user_id, + project_id, + token, + function(error, canAdmin) { + if (error != null) { + return next(error) + } + if (canAdmin) { + logger.log( + { user_id, project_id }, + 'allowing user admin access to project' + ) + return next() + } else { + logger.log( + { user_id, project_id }, + 'denying user admin access to project' + ) + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + } + ) + }) + }, + + ensureUserIsSiteAdmin(req, res, next) { + return AuthorizationMiddleware._getUserId(req, function(error, user_id) { + if (error != null) { + return next(error) + } + return AuthorizationManager.isUserSiteAdmin(user_id, function( + error, + isAdmin + ) { + if (error != null) { + return next(error) + } + if (isAdmin) { + logger.log({ user_id }, 'allowing user admin access to site') + return next() + } else { + logger.log({ user_id }, 'denying user admin access to site') + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + }) + }) + }, + + _getUserAndProjectId(req, callback) { + if (callback == null) { + callback = function(error, user_id, project_id) {} + } + const project_id = + (req.params != null ? req.params.project_id : undefined) || + (req.params != null ? req.params.Project_id : undefined) + if (project_id == null) { + return callback(new Error('Expected project_id in request parameters')) + } + if (!ObjectId.isValid(project_id)) { + return callback( + new Errors.NotFoundError(`invalid project_id: ${project_id}`) + ) + } + return AuthorizationMiddleware._getUserId(req, function(error, user_id) { + if (error != null) { + return callback(error) + } + return callback(null, user_id, project_id) + }) + }, + + _getUserId(req, callback) { + if (callback == null) { + callback = function(error, user_id) {} + } + const user_id = + AuthenticationController.getLoggedInUserId(req) || + __guard__(req != null ? req.oauth_user : undefined, x => x._id) || + null + return callback(null, user_id) + }, + + redirectToRestricted(req, res, next) { + // TODO: move this to throwing ForbiddenError + return res.redirect(`/restricted?from=${encodeURIComponent(req.url)}`) + }, + + restricted(req, res, next) { + if (AuthenticationController.isUserLoggedIn(req)) { + return res.render('user/restricted', { title: 'restricted' }) + } else { + const { from } = req.query + logger.log({ from }, 'redirecting to login') + const redirect_to = '/login' + if (from != null) { + AuthenticationController.setRedirectInSession(req, from) + } + return res.redirect(redirect_to) + } + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Authorization/PrivilegeLevels.js b/services/web/app/src/Features/Authorization/PrivilegeLevels.js new file mode 100644 index 0000000000..6e7c310cec --- /dev/null +++ b/services/web/app/src/Features/Authorization/PrivilegeLevels.js @@ -0,0 +1,8 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +module.exports = { + NONE: false, + READ_ONLY: 'readOnly', + READ_AND_WRITE: 'readAndWrite', + OWNER: 'owner' +} diff --git a/services/web/app/src/Features/Authorization/PublicAccessLevels.js b/services/web/app/src/Features/Authorization/PublicAccessLevels.js new file mode 100644 index 0000000000..ed918f4dab --- /dev/null +++ b/services/web/app/src/Features/Authorization/PublicAccessLevels.js @@ -0,0 +1,8 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +module.exports = { + READ_ONLY: 'readOnly', // LEGACY + READ_AND_WRITE: 'readAndWrite', // LEGACY + PRIVATE: 'private', + TOKEN_BASED: 'tokenBased' +} diff --git a/services/web/app/src/Features/Authorization/Sources.js b/services/web/app/src/Features/Authorization/Sources.js new file mode 100644 index 0000000000..271e7fca46 --- /dev/null +++ b/services/web/app/src/Features/Authorization/Sources.js @@ -0,0 +1,7 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +module.exports = { + INVITE: 'invite', + TOKEN: 'token', + OWNER: 'owner' +} diff --git a/services/web/app/src/Features/BetaProgram/BetaProgramController.js b/services/web/app/src/Features/BetaProgram/BetaProgramController.js new file mode 100644 index 0000000000..b5e82709e1 --- /dev/null +++ b/services/web/app/src/Features/BetaProgram/BetaProgramController.js @@ -0,0 +1,64 @@ +/* eslint-disable + camelcase, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let BetaProgramController +const BetaProgramHandler = require('./BetaProgramHandler') +const UserGetter = require('../User/UserGetter') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = BetaProgramController = { + optIn(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ user_id }, 'user opting in to beta program') + if (user_id == null) { + return next(new Error('no user id in session')) + } + return BetaProgramHandler.optIn(user_id, function(err) { + if (err) { + return next(err) + } + return res.redirect('/beta/participate') + }) + }, + + optOut(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ user_id }, 'user opting out of beta program') + if (user_id == null) { + return next(new Error('no user id in session')) + } + return BetaProgramHandler.optOut(user_id, function(err) { + if (err) { + return next(err) + } + return res.redirect('/beta/participate') + }) + }, + + optInPage(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ user_id }, 'showing beta participation page for user') + return UserGetter.getUser(user_id, function(err, user) { + if (err) { + logger.err({ err, user_id }, 'error fetching user') + return next(err) + } + return res.render('beta_program/opt_in', { + title: 'sharelatex_beta_program', + user, + languages: Settings.languages + }) + }) + } +} diff --git a/services/web/app/src/Features/BetaProgram/BetaProgramHandler.js b/services/web/app/src/Features/BetaProgram/BetaProgramHandler.js new file mode 100644 index 0000000000..adccaaa2d4 --- /dev/null +++ b/services/web/app/src/Features/BetaProgram/BetaProgramHandler.js @@ -0,0 +1,62 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let BetaProgramHandler +const { User } = require('../../models/User') +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') + +module.exports = BetaProgramHandler = { + optIn(user_id, callback) { + if (callback == null) { + callback = function(err) {} + } + return User.findById(user_id, function(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 + return user.save(function(err) { + if (err) { + logger.err({ err, user_id }, 'problem adding user to beta') + return callback(err) + } + return callback(null) + }) + }) + }, + + optOut(user_id, callback) { + if (callback == null) { + callback = function(err) {} + } + return User.findById(user_id, function(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 + return user.save(function(err) { + if (err) { + logger.err({ err, user_id }, 'problem removing user from beta') + return callback(err) + } + return callback(null) + }) + }) + } +} diff --git a/services/web/app/src/Features/Blog/BlogController.js b/services/web/app/src/Features/Blog/BlogController.js new file mode 100644 index 0000000000..465c9b3179 --- /dev/null +++ b/services/web/app/src/Features/Blog/BlogController.js @@ -0,0 +1,105 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let BlogController +const request = require('request') +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const ErrorController = require('../Errors/ErrorController') + +module.exports = BlogController = { + getPage(req, res, next) { + const url = req.url != null ? req.url.toLowerCase() : undefined + const blogUrl = `${settings.apis.blog.url}${url}` + + const extensionsToProxy = [ + '.png', + '.xml', + '.jpeg', + '.jpg', + '.json', + '.zip', + '.eps', + '.gif' + ] + + const shouldProxy = _.find( + extensionsToProxy, + extension => url.indexOf(extension) !== -1 + ) + + if (shouldProxy) { + return BlogController._directProxy(blogUrl, res) + } + + logger.log({ url }, 'proxying request to blog api') + return request.get(blogUrl, function(err, r, data) { + if ( + (r != null ? r.statusCode : undefined) === 404 || + (r != null ? r.statusCode : undefined) === 403 + ) { + return ErrorController.notFound(req, res, next) + } + if (err != null) { + return res.send(500) + } + data = data.trim() + try { + data = JSON.parse(data) + if ( + __guard__( + settings.cdn != null ? settings.cdn.web : undefined, + x => x.host + ) != null + ) { + if (data != null) { + data.content = __guard__( + data != null ? data.content : undefined, + x1 => + x1.replace( + /src="(\/[^"]+)"/g, + `src='${__guard__( + settings.cdn != null ? settings.cdn.web : undefined, + x2 => x2.host + )}$1'` + ) + ) + } + } + } catch (error) { + err = error + logger.err({ err, data }, 'error parsing data from data') + } + return res.render('blog/blog_holder', data) + }) + }, + + getIndexPage(req, res) { + req.url = '/blog/index.html' + return BlogController.getPage(req, res) + }, + + _directProxy(originUrl, res) { + const upstream = request.get(originUrl) + upstream.on('error', error => + logger.error({ err: error }, 'blog proxy error') + ) + return upstream.pipe(res) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Blog/BlogHandler.js b/services/web/app/src/Features/Blog/BlogHandler.js new file mode 100644 index 0000000000..b0e0d51aff --- /dev/null +++ b/services/web/app/src/Features/Blog/BlogHandler.js @@ -0,0 +1,49 @@ +/* eslint-disable + max-len, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let BlogHandler +const request = require('request') +const settings = require('settings-sharelatex') +const _ = require('underscore') +const logger = require('logger-sharelatex') + +module.exports = BlogHandler = { + getLatestAnnouncements(callback) { + const blogUrl = `${settings.apis.blog.url}/blog/latestannouncements.json` + const opts = { + url: blogUrl, + json: true, + timeout: 1000 + } + return request.get(opts, function(err, res, announcements) { + if (err != null) { + return callback(err) + } + if (res.statusCode !== 200) { + return callback('blog announcement returned non 200') + } + logger.log( + { + announcementsLength: + announcements != null ? announcements.length : undefined + }, + 'announcements returned' + ) + announcements = _.map(announcements, function(announcement) { + announcement.date = new Date(announcement.date) + return announcement + }) + return callback(err, announcements) + }) + } +} diff --git a/services/web/app/src/Features/BrandVariations/BrandVariationsHandler.js b/services/web/app/src/Features/BrandVariations/BrandVariationsHandler.js new file mode 100644 index 0000000000..89e25ad8a7 --- /dev/null +++ b/services/web/app/src/Features/BrandVariations/BrandVariationsHandler.js @@ -0,0 +1,98 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let BrandVariationsHandler +const url = require('url') +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const V1Api = require('../V1/V1Api') + +module.exports = BrandVariationsHandler = { + getBrandVariationById(brandVariationId, callback) { + if (callback == null) { + callback = function(error, brandVariationDetails) {} + } + if (brandVariationId == null || brandVariationId === '') { + return callback(new Error('Branding variation id not provided')) + } + logger.log({ brandVariationId }, 'fetching brand variation details from v1') + return V1Api.request( + { + uri: `/api/v2/brand_variations/${brandVariationId}` + }, + function(error, response, brandVariationDetails) { + if (error != null) { + logger.err( + { brandVariationId, error }, + 'error getting brand variation details' + ) + return callback(error) + } + _formatBrandVariationDetails(brandVariationDetails) + return callback(null, brandVariationDetails) + } + ) + } +} + +var _formatBrandVariationDetails = function(details) { + if (details.export_url != null) { + details.export_url = _setV1AsHostIfRelativeURL(details.export_url) + } + if (details.home_url != null) { + details.home_url = _setV1AsHostIfRelativeURL(details.home_url) + } + if (details.logo_url != null) { + details.logo_url = _setV1AsHostIfRelativeURL(details.logo_url) + } + if (details.journal_guidelines_url != null) { + details.journal_guidelines_url = _setV1AsHostIfRelativeURL( + details.journal_guidelines_url + ) + } + if (details.journal_cover_url != null) { + details.journal_cover_url = _setV1AsHostIfRelativeURL( + details.journal_cover_url + ) + } + if (details.submission_confirmation_page_logo_url != null) { + details.submission_confirmation_page_logo_url = _setV1AsHostIfRelativeURL( + details.submission_confirmation_page_logo_url + ) + } + if (details.publish_menu_icon != null) { + return (details.publish_menu_icon = _setV1AsHostIfRelativeURL( + details.publish_menu_icon + )) + } +} + +var _setV1AsHostIfRelativeURL = urlString => + // The first argument is the base URL to resolve against if the second argument is not absolute. + // As it only applies if the second argument is not absolute, we can use it to transform relative URLs into + // absolute ones using v1 as the host. If the URL is absolute (e.g. a filepicker one), then the base + // argument is just ignored + url.resolve( + __guard__( + __guard__(settings != null ? settings.apis : undefined, x1 => x1.v1), + x => x.url + ), + urlString + ) +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Captcha/CaptchaMiddleware.js b/services/web/app/src/Features/Captcha/CaptchaMiddleware.js new file mode 100644 index 0000000000..a0b8805269 --- /dev/null +++ b/services/web/app/src/Features/Captcha/CaptchaMiddleware.js @@ -0,0 +1,68 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CaptchaMiddleware +const request = require('request') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') + +module.exports = CaptchaMiddleware = { + validateCaptcha(action) { + return function(req, res, next) { + if ( + (Settings.recaptcha != null ? Settings.recaptcha.siteKey : undefined) == + null + ) { + return next() + } + const inviteAndCaptchaDisabled = + action === 'invite' && Settings.recaptcha.disabled.invite + const registerAndCaptchaDisabled = + action === 'register' && Settings.recaptcha.disabled.register + if (inviteAndCaptchaDisabled || registerAndCaptchaDisabled) { + return next() + } + const response = req.body['g-recaptcha-response'] + const options = { + form: { + secret: Settings.recaptcha.secretKey, + response + }, + json: true + } + return request.post( + 'https://www.google.com/recaptcha/api/siteverify', + options, + function(error, response, body) { + if (error != null) { + return next(error) + } + if (!(body != null ? body.success : undefined)) { + logger.warn( + { statusCode: response.statusCode, body }, + 'failed recaptcha siteverify request' + ) + return res.status(400).send({ + errorReason: 'cannot_verify_user_not_robot', + message: { + text: + 'Sorry, we could not verify that you are not a robot. Please check that Google reCAPTCHA is not being blocked by an ad blocker or firewall.' + } + }) + } else { + return next() + } + } + ) + } + } +} diff --git a/services/web/app/src/Features/Chat/ChatApiHandler.js b/services/web/app/src/Features/Chat/ChatApiHandler.js new file mode 100644 index 0000000000..2f0a6689ae --- /dev/null +++ b/services/web/app/src/Features/Chat/ChatApiHandler.js @@ -0,0 +1,184 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ChatApiHandler +const request = require('request') +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') + +module.exports = ChatApiHandler = { + _apiRequest(opts, callback) { + if (callback == null) { + callback = function(error, data) {} + } + return request(opts, function(error, response, data) { + if (error != null) { + return callback(error) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + return callback(null, data) + } else { + error = new Error( + `chat api returned non-success code: ${response.statusCode}` + ) + error.statusCode = response.statusCode + logger.error({ err: error, opts }, 'error sending request to chat api') + return callback(error) + } + }) + }, + + sendGlobalMessage(project_id, user_id, content, callback) { + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/messages`, + method: 'POST', + json: { user_id, content } + }, + callback + ) + }, + + getGlobalMessages(project_id, limit, before, callback) { + const qs = {} + if (limit != null) { + qs.limit = limit + } + if (before != null) { + qs.before = before + } + + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/messages`, + method: 'GET', + qs, + json: true + }, + callback + ) + }, + + sendComment(project_id, thread_id, user_id, content, callback) { + if (callback == null) { + callback = function(error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/thread/${thread_id}/messages`, + method: 'POST', + json: { user_id, content } + }, + callback + ) + }, + + getThreads(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/threads`, + method: 'GET', + json: true + }, + callback + ) + }, + + resolveThread(project_id, thread_id, user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/thread/${thread_id}/resolve`, + method: 'POST', + json: { user_id } + }, + callback + ) + }, + + reopenThread(project_id, thread_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/thread/${thread_id}/reopen`, + method: 'POST' + }, + callback + ) + }, + + deleteThread(project_id, thread_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/thread/${thread_id}`, + method: 'DELETE' + }, + callback + ) + }, + + editMessage(project_id, thread_id, message_id, content, callback) { + if (callback == null) { + callback = function(error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/thread/${thread_id}/messages/${message_id}/edit`, + method: 'POST', + json: { + content + } + }, + callback + ) + }, + + deleteMessage(project_id, thread_id, message_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${ + settings.apis.chat.internal_url + }/project/${project_id}/thread/${thread_id}/messages/${message_id}`, + method: 'DELETE' + }, + callback + ) + } +} diff --git a/services/web/app/src/Features/Chat/ChatController.js b/services/web/app/src/Features/Chat/ChatController.js new file mode 100644 index 0000000000..b8cc00924b --- /dev/null +++ b/services/web/app/src/Features/Chat/ChatController.js @@ -0,0 +1,143 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ChatController +const ChatApiHandler = require('./ChatApiHandler') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const logger = require('logger-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') +const UserInfoManager = require('../User/UserInfoManager') +const UserInfoController = require('../User/UserInfoController') +const async = require('async') + +module.exports = ChatController = { + sendMessage(req, res, next) { + const { project_id } = req.params + const { content } = req.body + const user_id = AuthenticationController.getLoggedInUserId(req) + if (user_id == null) { + const err = new Error('no logged-in user') + return next(err) + } + return ChatApiHandler.sendGlobalMessage( + project_id, + user_id, + content, + function(err, message) { + if (err != null) { + return next(err) + } + return UserInfoManager.getPersonalInfo(message.user_id, function( + err, + user + ) { + if (err != null) { + return next(err) + } + message.user = UserInfoController.formatPersonalInfo(user) + EditorRealTimeController.emitToRoom( + project_id, + 'new-chat-message', + message + ) + return res.send(204) + }) + } + ) + }, + + getMessages(req, res, next) { + const { project_id } = req.params + const { query } = req + logger.log({ project_id, query }, 'getting messages') + return ChatApiHandler.getGlobalMessages( + project_id, + query.limit, + query.before, + function(err, messages) { + if (err != null) { + return next(err) + } + return ChatController._injectUserInfoIntoThreads( + { global: { messages } }, + function(err) { + if (err != null) { + return next(err) + } + logger.log( + { length: messages != null ? messages.length : undefined }, + 'sending messages to client' + ) + return res.json(messages) + } + ) + } + ) + }, + + _injectUserInfoIntoThreads(threads, callback) { + // There will be a lot of repitition of user_ids, so first build a list + // of unique ones to perform db look ups on, then use these to populate the + // user fields + let message, thread, thread_id, user_id + if (callback == null) { + callback = function(error, threads) {} + } + const user_ids = {} + for (thread_id in threads) { + thread = threads[thread_id] + if (thread.resolved) { + user_ids[thread.resolved_by_user_id] = true + } + for (message of Array.from(thread.messages)) { + user_ids[message.user_id] = true + } + } + + const jobs = [] + const users = {} + for (user_id in user_ids) { + const _ = user_ids[user_id] + ;(user_id => + jobs.push(cb => + UserInfoManager.getPersonalInfo(user_id, function(err, user) { + if (typeof error !== 'undefined' && error !== null) { + return cb(error) + } + user = UserInfoController.formatPersonalInfo(user) + users[user_id] = user + return cb() + }) + ))(user_id) + } + + return async.series(jobs, function(error) { + if (error != null) { + return callback(error) + } + for (thread_id in threads) { + thread = threads[thread_id] + if (thread.resolved) { + thread.resolved_by_user = users[thread.resolved_by_user_id] + } + for (message of Array.from(thread.messages)) { + message.user = users[message.user_id] + } + } + return callback(null, threads) + }) + } +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsController.js b/services/web/app/src/Features/Collaborators/CollaboratorsController.js new file mode 100644 index 0000000000..29f018668b --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsController.js @@ -0,0 +1,109 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CollaboratorsController +const ProjectGetter = require('../Project/ProjectGetter') +const CollaboratorsHandler = require('./CollaboratorsHandler') +const ProjectEditorHandler = require('../Project/ProjectEditorHandler') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const LimitationsManager = require('../Subscription/LimitationsManager') +const UserGetter = require('../User/UserGetter') +const EmailHelper = require('../Helpers/EmailHelper') +const logger = require('logger-sharelatex') + +module.exports = CollaboratorsController = { + removeUserFromProject(req, res, next) { + const project_id = req.params.Project_id + const { user_id } = req.params + return CollaboratorsController._removeUserIdFromProject( + project_id, + user_id, + function(error) { + if (error != null) { + return next(error) + } + EditorRealTimeController.emitToRoom( + project_id, + 'project:membership:changed', + { members: true } + ) + return res.sendStatus(204) + } + ) + }, + + removeSelfFromProject(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + const user_id = __guard__( + req.session != null ? req.session.user : undefined, + x => x._id + ) + return CollaboratorsController._removeUserIdFromProject( + project_id, + user_id, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + }, + + _removeUserIdFromProject(project_id, user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return CollaboratorsHandler.removeUserFromProject( + project_id, + user_id, + function(error) { + if (error != null) { + return callback(error) + } + EditorRealTimeController.emitToRoom( + project_id, + 'userRemovedFromProject', + user_id + ) + return callback() + } + ) + }, + + getAllMembers(req, res, next) { + const projectId = req.params.Project_id + logger.log({ projectId }, 'getting all active members for project') + return CollaboratorsHandler.getAllInvitedMembers(projectId, function( + err, + members + ) { + if (err != null) { + logger.err({ projectId }, 'error getting members for project') + return next(err) + } + return res.json({ members }) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsEmailHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsEmailHandler.js new file mode 100644 index 0000000000..812c5b3cf4 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsEmailHandler.js @@ -0,0 +1,49 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CollaboratorsEmailHandler +const { Project } = require('../../models/Project') +const EmailHandler = require('../Email/EmailHandler') +const Settings = require('settings-sharelatex') + +module.exports = CollaboratorsEmailHandler = { + _buildInviteUrl(project, invite) { + return ( + `${Settings.siteUrl}/project/${project._id}/invite/token/${ + invite.token + }?` + + [ + `project_name=${encodeURIComponent(project.name)}`, + `user_first_name=${encodeURIComponent(project.owner_ref.first_name)}` + ].join('&') + ) + }, + + notifyUserOfProjectInvite(project_id, email, invite, sendingUser, callback) { + return Project.findOne({ _id: project_id }) + .select('name owner_ref') + .populate('owner_ref') + .exec(function(err, project) { + const emailOptions = { + to: email, + replyTo: project.owner_ref.email, + project: { + name: project.name + }, + inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite), + owner: project.owner_ref, + sendingUser_id: sendingUser._id + } + return EmailHandler.sendEmail('projectInvite', emailOptions, callback) + }) + } +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js new file mode 100644 index 0000000000..f9535b4249 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js @@ -0,0 +1,635 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CollaboratorsHandler +const UserCreator = require('../User/UserCreator') +const { Project } = require('../../models/Project') +const ProjectGetter = require('../Project/ProjectGetter') +const logger = require('logger-sharelatex') +const UserGetter = require('../User/UserGetter') +const ContactManager = require('../Contacts/ContactManager') +const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler') +const async = require('async') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const Errors = require('../Errors/Errors') +const EmailHelper = require('../Helpers/EmailHelper') +const ProjectEditorHandler = require('../Project/ProjectEditorHandler') +const Sources = require('../Authorization/Sources') +const { ObjectId } = require('mongojs') + +module.exports = CollaboratorsHandler = { + getMemberIdsWithPrivilegeLevels(project_id, callback) { + if (callback == null) { + callback = function(error, members) {} + } + const projection = { + owner_ref: 1, + collaberator_refs: 1, + readOnly_refs: 1, + tokenAccessReadOnly_refs: 1, + tokenAccessReadAndWrite_refs: 1, + publicAccesLevel: 1 + } + return ProjectGetter.getProject(project_id, projection, function( + error, + project + ) { + let member_id + if (error != null) { + return callback(error) + } + if (project == null) { + return callback( + new Errors.NotFoundError(`no project found with id ${project_id}`) + ) + } + const members = [] + members.push({ + id: project.owner_ref.toString(), + privilegeLevel: PrivilegeLevels.OWNER, + source: Sources.OWNER + }) + for (member_id of Array.from(project.collaberator_refs || [])) { + members.push({ + id: member_id.toString(), + privilegeLevel: PrivilegeLevels.READ_AND_WRITE, + source: Sources.INVITE + }) + } + for (member_id of Array.from(project.readOnly_refs || [])) { + members.push({ + id: member_id.toString(), + privilegeLevel: PrivilegeLevels.READ_ONLY, + source: Sources.INVITE + }) + } + if (project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED) { + for (member_id of Array.from( + project.tokenAccessReadAndWrite_refs || [] + )) { + members.push({ + id: member_id.toString(), + privilegeLevel: PrivilegeLevels.READ_AND_WRITE, + source: Sources.TOKEN + }) + } + for (member_id of Array.from(project.tokenAccessReadOnly_refs || [])) { + members.push({ + id: member_id.toString(), + privilegeLevel: PrivilegeLevels.READ_ONLY, + source: Sources.TOKEN + }) + } + } + return callback(null, members) + }) + }, + + getMemberIds(project_id, callback) { + if (callback == null) { + callback = function(error, member_ids) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (error != null) { + return callback(error) + } + return callback(null, members.map(m => m.id)) + } + ) + }, + + getInvitedMemberIds(project_id, callback) { + if (callback == null) { + callback = function(error, member_ids) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (error != null) { + return callback(error) + } + return callback( + null, + members.filter(m => m.source !== Sources.TOKEN).map(m => m.id) + ) + } + ) + }, + + USER_PROJECTION: { + _id: 1, + email: 1, + features: 1, + first_name: 1, + last_name: 1, + signUpDate: 1 + }, + _loadMembers(members, callback) { + if (callback == null) { + callback = function(error, users) {} + } + const result = [] + return async.mapLimit( + members, + 3, + (member, cb) => + UserGetter.getUserOrUserStubById( + member.id, + CollaboratorsHandler.USER_PROJECTION, + function(error, user) { + if (error != null) { + return cb(error) + } + if (user != null) { + result.push({ user, privilegeLevel: member.privilegeLevel }) + } + return cb() + } + ), + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, result) + } + ) + }, + + getMembersWithPrivilegeLevels(project_id, callback) { + if (callback == null) { + callback = function(error, members) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (members == null) { + members = [] + } + if (error != null) { + return callback(error) + } + return CollaboratorsHandler._loadMembers(members, (error, users) => + callback(error, users) + ) + } + ) + }, + + getInvitedMembersWithPrivilegeLevels(project_id, callback) { + if (callback == null) { + callback = function(error, members) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (members == null) { + members = [] + } + if (error != null) { + return callback(error) + } + members = members.filter(m => m.source !== Sources.TOKEN) + return CollaboratorsHandler._loadMembers(members, (error, users) => + callback(error, users) + ) + } + ) + }, + + getTokenMembersWithPrivilegeLevels(project_id, callback) { + if (callback == null) { + callback = function(error, members) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (members == null) { + members = [] + } + if (error != null) { + return callback(error) + } + members = members.filter(m => m.source === Sources.TOKEN) + return CollaboratorsHandler._loadMembers(members, (error, users) => + callback(error, users) + ) + } + ) + }, + + getMemberIdPrivilegeLevel(user_id, project_id, callback) { + // In future if the schema changes and getting all member ids is more expensive (multiple documents) + // then optimise this. + if (callback == null) { + callback = function(error, privilegeLevel) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (members == null) { + members = [] + } + if (error != null) { + return callback(error) + } + for (let member of Array.from(members)) { + if ( + member.id === (user_id != null ? user_id.toString() : undefined) + ) { + return callback(null, member.privilegeLevel) + } + } + return callback(null, PrivilegeLevels.NONE) + } + ) + }, + + getInvitedMemberCount(project_id, callback) { + if (callback == null) { + callback = function(error, count) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (error != null) { + return callback(error) + } + return callback( + null, + (members || []).filter(m => m.source !== Sources.TOKEN).length + ) + } + ) + }, + + getInvitedCollaboratorCount(project_id, callback) { + if (callback == null) { + callback = function(error, count) {} + } + return CollaboratorsHandler.getInvitedMemberCount(project_id, function( + error, + count + ) { + if (error != null) { + return callback(error) + } + return callback(null, count - 1) + }) + }, // Don't count project owner + + isUserInvitedMemberOfProject(user_id, project_id, callback) { + if (callback == null) { + callback = function(error, isMember, privilegeLevel) {} + } + return CollaboratorsHandler.getMemberIdsWithPrivilegeLevels( + project_id, + function(error, members) { + if (members == null) { + members = [] + } + if (error != null) { + return callback(error) + } + for (let member of Array.from(members)) { + if ( + member.id.toString() === user_id.toString() && + member.source !== Sources.TOKEN + ) { + return callback(null, true, member.privilegeLevel) + } + } + return callback(null, false, null) + } + ) + }, + + getProjectsUserIsMemberOf(user_id, fields, callback) { + if (callback == null) { + callback = function(error, results) {} + } + return async.mapLimit( + [ + { collaberator_refs: user_id }, + { readOnly_refs: user_id }, + { + tokenAccessReadAndWrite_refs: user_id, + publicAccesLevel: PublicAccessLevels.TOKEN_BASED + }, + { + tokenAccessReadOnly_refs: user_id, + publicAccesLevel: PublicAccessLevels.TOKEN_BASED + } + ], + 2, + (query, cb) => Project.find(query, fields, cb), + function(error, results) { + if (error != null) { + return callback(error) + } + const projects = { + readAndWrite: results[0], + readOnly: results[1], + tokenReadAndWrite: results[2], + tokenReadOnly: results[3] + } + return callback(null, projects) + } + ) + }, + + removeUserFromProject(project_id, user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ user_id, project_id }, 'removing user') + const conditions = { _id: project_id } + const update = { $pull: {} } + update['$pull'] = { + collaberator_refs: user_id, + readOnly_refs: user_id, + tokenAccessReadOnly_refs: user_id, + tokenAccessReadAndWrite_refs: user_id + } + return Project.update(conditions, update, function(err) { + if (err != null) { + logger.error( + { err }, + 'problem removing user from project collaberators' + ) + } + return callback(err) + }) + }, + + removeUserFromAllProjets(user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return CollaboratorsHandler.getProjectsUserIsMemberOf( + user_id, + { _id: 1 }, + function( + error, + { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } + ) { + if (error != null) { + return callback(error) + } + const allProjects = readAndWrite + .concat(readOnly) + .concat(tokenReadAndWrite) + .concat(tokenReadOnly) + const jobs = [] + for (let project of Array.from(allProjects)) { + ;(project => + jobs.push(function(cb) { + if (project == null) { + return cb() + } + return CollaboratorsHandler.removeUserFromProject( + project._id, + user_id, + cb + ) + }))(project) + } + return async.series(jobs, callback) + } + ) + }, + + addUserIdToProject( + project_id, + adding_user_id, + user_id, + privilegeLevel, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + return ProjectGetter.getProject( + project_id, + { collaberator_refs: 1, readOnly_refs: 1 }, + function(error, project) { + let level + if (error != null) { + return callback(error) + } + let existing_users = project.collaberator_refs || [] + existing_users = existing_users.concat(project.readOnly_refs || []) + existing_users = existing_users.map(u => u.toString()) + if (existing_users.indexOf(user_id.toString()) > -1) { + return callback(null) // User already in Project + } + + if (privilegeLevel === PrivilegeLevels.READ_AND_WRITE) { + level = { collaberator_refs: user_id } + logger.log( + { privileges: 'readAndWrite', user_id, project_id }, + 'adding user' + ) + } else if (privilegeLevel === PrivilegeLevels.READ_ONLY) { + level = { readOnly_refs: user_id } + logger.log( + { privileges: 'readOnly', user_id, project_id }, + 'adding user' + ) + } else { + return callback( + new Error(`unknown privilegeLevel: ${privilegeLevel}`) + ) + } + + if (adding_user_id) { + ContactManager.addContact(adding_user_id, user_id) + } + + return Project.update( + { _id: project_id }, + { $addToSet: level }, + function(error) { + if (error != null) { + return callback(error) + } + // Flush to TPDS in background to add files to collaborator's Dropbox + const ProjectEntityHandler = require('../Project/ProjectEntityHandler') + ProjectEntityHandler.flushProjectToThirdPartyDataStore( + project_id, + function(error) { + if (error != null) { + return logger.error( + { err: error, project_id, user_id }, + 'error flushing to TPDS after adding collaborator' + ) + } + } + ) + return callback() + } + ) + } + ) + }, + + getAllInvitedMembers(projectId, callback) { + if (callback == null) { + callback = function(err, members) {} + } + logger.log({ projectId }, 'fetching all members') + return CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels( + projectId, + function(error, rawMembers) { + if (error != null) { + logger.err({ projectId, error }, 'error getting members for project') + return callback(error) + } + const { + owner, + members + } = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) + return callback(null, members) + } + ) + }, + + userIsTokenMember(userId, projectId, callback) { + if (callback == null) { + callback = function(err, isTokenMember) {} + } + userId = ObjectId(userId.toString()) + projectId = ObjectId(projectId.toString()) + return Project.findOne( + { + _id: projectId, + $or: [ + { tokenAccessReadOnly_refs: userId }, + { tokenAccessReadAndWrite_refs: userId } + ] + }, + { + _id: 1 + }, + function(err, project) { + if (err != null) { + return callback(err) + } + return callback(null, project != null) + } + ) + }, + + transferProjects(from_user_id, to_user_id, callback) { + if (callback == null) { + callback = function(err, projects) {} + } + const MEMBER_KEYS = ['collaberator_refs', 'readOnly_refs'] + + // Find all the projects this user is part of so we can flush them to TPDS + let query = { + $or: [{ owner_ref: from_user_id }].concat( + MEMBER_KEYS.map(function(key) { + const q = {} + q[key] = from_user_id + return q + }) + ) // [{ collaberator_refs: from_user_id }, ...] + } + return Project.find(query, { _id: 1 }, function(error, projects) { + if (projects == null) { + projects = [] + } + if (error != null) { + return callback(error) + } + + const project_ids = projects.map(p => p._id) + logger.log( + { project_ids, from_user_id, to_user_id }, + 'transferring projects' + ) + + const update_jobs = [] + update_jobs.push(cb => + Project.update( + { owner_ref: from_user_id }, + { $set: { owner_ref: to_user_id } }, + { multi: true }, + cb + ) + ) + for (let key of Array.from(MEMBER_KEYS)) { + ;(key => + update_jobs.push(function(cb) { + query = {} + const addNewUserUpdate = { $addToSet: {} } + const removeOldUserUpdate = { $pull: {} } + query[key] = from_user_id + removeOldUserUpdate.$pull[key] = from_user_id + addNewUserUpdate.$addToSet[key] = to_user_id + // Mongo won't let us pull and addToSet in the same query, so do it in + // two. Note we need to add first, since the query is based on the old user. + return Project.update( + query, + addNewUserUpdate, + { multi: true }, + function(error) { + if (error != null) { + return cb(error) + } + return Project.update( + query, + removeOldUserUpdate, + { multi: true }, + cb + ) + } + ) + }))(key) + } + + // Flush each project to TPDS to add files to new user's Dropbox + const ProjectEntityHandler = require('../Project/ProjectEntityHandler') + const flush_jobs = [] + for (let project_id of Array.from(project_ids)) { + ;(project_id => + flush_jobs.push(cb => + ProjectEntityHandler.flushProjectToThirdPartyDataStore( + project_id, + cb + ) + ))(project_id) + } + + // Flush in background, no need to block on this + async.series(flush_jobs, function(error) { + if (error != null) { + return logger.err( + { err: error, project_ids, from_user_id, to_user_id }, + 'error flushing tranferred projects to TPDS' + ) + } + }) + + return async.series(update_jobs, function(err) { + logger.log('flushed transferred projects to TPDS') + return callback(err) + }) + }) + } +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js new file mode 100644 index 0000000000..64d82b04d6 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js @@ -0,0 +1,368 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CollaboratorsInviteController +const ProjectGetter = require('../Project/ProjectGetter') +const LimitationsManager = require('../Subscription/LimitationsManager') +const UserGetter = require('../User/UserGetter') +const CollaboratorsHandler = require('./CollaboratorsHandler') +const CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') +const EmailHelper = require('../Helpers/EmailHelper') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const AnalyticsManger = require('../Analytics/AnalyticsManager') +const AuthenticationController = require('../Authentication/AuthenticationController') +const rateLimiter = require('../../infrastructure/RateLimiter') +const request = require('request') + +module.exports = CollaboratorsInviteController = { + getAllInvites(req, res, next) { + const projectId = req.params.Project_id + logger.log({ projectId }, 'getting all active invites for project') + return CollaboratorsInviteHandler.getAllInvites(projectId, function( + err, + invites + ) { + if (err != null) { + logger.err({ projectId }, 'error getting invites for project') + return next(err) + } + return res.json({ invites }) + }) + }, + + _checkShouldInviteEmail(email, callback) { + if (callback == null) { + callback = function(err, shouldAllowInvite) {} + } + if (Settings.restrictInvitesToExistingAccounts === true) { + logger.log({ email }, 'checking if user exists with this email') + return UserGetter.getUserByAnyEmail(email, { _id: 1 }, function( + err, + user + ) { + if (err != null) { + return callback(err) + } + const userExists = + user != null && (user != null ? user._id : undefined) != null + return callback(null, userExists) + }) + } else { + return callback(null, true) + } + }, + + _checkRateLimit(user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return LimitationsManager.allowedNumberOfCollaboratorsForUser( + user_id, + function(err, collabLimit) { + if (collabLimit == null) { + collabLimit = 1 + } + if (err != null) { + return callback(err) + } + if (collabLimit === -1) { + collabLimit = 20 + } + collabLimit = collabLimit * 10 + const opts = { + endpointName: 'invite-to-project-by-user-id', + timeInterval: 60 * 30, + subjectName: user_id, + throttle: collabLimit + } + return rateLimiter.addCount(opts, callback) + } + ) + }, + + inviteToProject(req, res, next) { + const projectId = req.params.Project_id + let { email } = req.body + const sendingUser = AuthenticationController.getSessionUser(req) + const sendingUserId = sendingUser._id + if (email === sendingUser.email) { + logger.log( + { projectId, email, sendingUserId }, + 'cannot invite yourself to project' + ) + return res.json({ invite: null, error: 'cannot_invite_self' }) + } + logger.log({ projectId, email, sendingUserId }, 'inviting to project') + return LimitationsManager.canAddXCollaborators( + projectId, + 1, + (error, allowed) => { + let privileges + if (error != null) { + return next(error) + } + if (!allowed) { + logger.log( + { projectId, email, sendingUserId }, + 'not allowed to invite more users to project' + ) + return res.json({ invite: null }) + } + ;({ email, privileges } = req.body) + email = EmailHelper.parseEmail(email) + if (email == null || email === '') { + logger.log( + { projectId, email, sendingUserId }, + 'invalid email address' + ) + return res.status(400).send({ errorReason: 'invalid_email' }) + } + return CollaboratorsInviteController._checkRateLimit( + sendingUserId, + function(error, underRateLimit) { + if (error != null) { + return next(error) + } + if (!underRateLimit) { + return res.sendStatus(429) + } + return CollaboratorsInviteController._checkShouldInviteEmail( + email, + function(err, shouldAllowInvite) { + if (err != null) { + logger.err( + { err, email, projectId, sendingUserId }, + 'error checking if we can invite this email address' + ) + return next(err) + } + if (!shouldAllowInvite) { + logger.log( + { email, projectId, sendingUserId }, + 'not allowed to send an invite to this email address' + ) + return res.json({ + invite: null, + error: 'cannot_invite_non_user' + }) + } + return CollaboratorsInviteHandler.inviteToProject( + projectId, + sendingUser, + email, + privileges, + function(err, invite) { + if (err != null) { + logger.err( + { projectId, email, sendingUserId }, + 'error creating project invite' + ) + return next(err) + } + logger.log( + { projectId, email, sendingUserId }, + 'invite created' + ) + EditorRealTimeController.emitToRoom( + projectId, + 'project:membership:changed', + { invites: true } + ) + return res.json({ invite }) + } + ) + } + ) + } + ) + } + ) + }, + + revokeInvite(req, res, next) { + const projectId = req.params.Project_id + const inviteId = req.params.invite_id + logger.log({ projectId, inviteId }, 'revoking invite') + return CollaboratorsInviteHandler.revokeInvite( + projectId, + inviteId, + function(err) { + if (err != null) { + logger.err({ projectId, inviteId }, 'error revoking invite') + return next(err) + } + EditorRealTimeController.emitToRoom( + projectId, + 'project:membership:changed', + { invites: true } + ) + return res.sendStatus(201) + } + ) + }, + + resendInvite(req, res, next) { + const projectId = req.params.Project_id + const inviteId = req.params.invite_id + logger.log({ projectId, inviteId }, 'resending invite') + const sendingUser = AuthenticationController.getSessionUser(req) + return CollaboratorsInviteController._checkRateLimit( + sendingUser._id, + function(error, underRateLimit) { + if (error != null) { + return next(error) + } + if (!underRateLimit) { + return res.sendStatus(429) + } + return CollaboratorsInviteHandler.resendInvite( + projectId, + sendingUser, + inviteId, + function(err) { + if (err != null) { + logger.err({ projectId, inviteId }, 'error resending invite') + return next(err) + } + return res.sendStatus(201) + } + ) + } + ) + }, + + viewInvite(req, res, next) { + const projectId = req.params.Project_id + const { token } = req.params + const _renderInvalidPage = function() { + logger.log( + { projectId, token }, + 'invite not valid, rendering not-valid page' + ) + return res.render('project/invite/not-valid', { title: 'Invalid Invite' }) + } + // check if the user is already a member of the project + const currentUser = AuthenticationController.getSessionUser(req) + return CollaboratorsHandler.isUserInvitedMemberOfProject( + currentUser._id, + projectId, + function(err, isMember, _privilegeLevel) { + if (err != null) { + logger.err( + { err, projectId }, + 'error checking if user is member of project' + ) + return next(err) + } + if (isMember) { + logger.log( + { projectId, userId: currentUser._id }, + 'user is already a member of this project, redirecting' + ) + return res.redirect(`/project/${projectId}`) + } + // get the invite + return CollaboratorsInviteHandler.getInviteByToken( + projectId, + token, + function(err, invite) { + if (err != null) { + logger.err({ projectId, token }, 'error getting invite by token') + return next(err) + } + // check if invite is gone, or otherwise non-existent + if (invite == null) { + logger.log({ projectId, token }, 'no invite found for this token') + return _renderInvalidPage() + } + // check the user who sent the invite exists + return UserGetter.getUser( + { _id: invite.sendingUserId }, + { email: 1, first_name: 1, last_name: 1 }, + function(err, owner) { + if (err != null) { + logger.err({ err, projectId }, 'error getting project owner') + return next(err) + } + if (owner == null) { + logger.log({ projectId }, 'no project owner found') + return _renderInvalidPage() + } + // fetch the project name + return ProjectGetter.getProject(projectId, {}, function( + err, + project + ) { + if (err != null) { + logger.err({ err, projectId }, 'error getting project') + return next(err) + } + if (project == null) { + logger.log({ projectId }, 'no project found') + return _renderInvalidPage() + } + // finally render the invite + return res.render('project/invite/show', { + invite, + project, + owner, + title: 'Project Invite' + }) + }) + } + ) + } + ) + } + ) + }, + + acceptInvite(req, res, next) { + const projectId = req.params.Project_id + const { token } = req.params + const currentUser = AuthenticationController.getSessionUser(req) + logger.log( + { projectId, userId: currentUser._id, token }, + 'got request to accept invite' + ) + return CollaboratorsInviteHandler.acceptInvite( + projectId, + token, + currentUser, + function(err) { + if (err != null) { + logger.err({ projectId, token }, 'error accepting invite by token') + return next(err) + } + EditorRealTimeController.emitToRoom( + projectId, + 'project:membership:changed', + { invites: true, members: true } + ) + AnalyticsManger.recordEvent(currentUser._id, 'project-invite-accept', { + projectId, + userId: currentUser._id + }) + if (req.xhr) { + return res.sendStatus(204) // Done async via project page notification + } else { + return res.redirect(`/project/${projectId}`) + } + } + ) + } +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js new file mode 100644 index 0000000000..5e8950f74c --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js @@ -0,0 +1,330 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CollaboratorsInviteHandler +const { ProjectInvite } = require('../../models/ProjectInvite') +const logger = require('logger-sharelatex') +const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler') +const CollaboratorsHandler = require('./CollaboratorsHandler') +const UserGetter = require('../User/UserGetter') +const ProjectGetter = require('../Project/ProjectGetter') +const Async = require('async') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const Errors = require('../Errors/Errors') +const Crypto = require('crypto') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') + +module.exports = CollaboratorsInviteHandler = { + getAllInvites(projectId, callback) { + if (callback == null) { + callback = function(err, invites) {} + } + logger.log({ projectId }, 'fetching invites for project') + return ProjectInvite.find({ projectId }, function(err, invites) { + if (err != null) { + logger.err({ err, projectId }, 'error getting invites from mongo') + return callback(err) + } + logger.log( + { projectId, count: invites.length }, + 'found invites for project' + ) + return callback(null, invites) + }) + }, + + getInviteCount(projectId, callback) { + if (callback == null) { + callback = function(err, count) {} + } + logger.log({ projectId }, 'counting invites for project') + return ProjectInvite.count({ projectId }, function(err, count) { + if (err != null) { + logger.err({ err, projectId }, 'error getting invites from mongo') + return callback(err) + } + return callback(null, count) + }) + }, + + _trySendInviteNotification(projectId, sendingUser, invite, callback) { + if (callback == null) { + callback = function(err) {} + } + const { email } = invite + return UserGetter.getUserByAnyEmail(email, { _id: 1 }, function( + err, + existingUser + ) { + if (err != null) { + logger.err({ projectId, email }, 'error checking if user exists') + return callback(err) + } + if (existingUser == null) { + logger.log({ projectId, email }, 'no existing user found, returning') + return callback(null) + } + return ProjectGetter.getProject(projectId, { _id: 1, name: 1 }, function( + err, + project + ) { + if (err != null) { + logger.err({ projectId, email }, 'error getting project') + return callback(err) + } + if (project == null) { + logger.log( + { projectId }, + 'no project found while sending notification, returning' + ) + return callback(null) + } + return NotificationsBuilder.projectInvite( + invite, + project, + sendingUser, + existingUser + ).create(callback) + }) + }) + }, + + _tryCancelInviteNotification(inviteId, callback) { + if (callback == null) { + callback = function() {} + } + return NotificationsBuilder.projectInvite( + { _id: inviteId }, + null, + null, + null + ).read(callback) + }, + + _sendMessages(projectId, sendingUser, invite, callback) { + if (callback == null) { + callback = function(err) {} + } + logger.log( + { projectId, inviteId: invite._id }, + 'sending notification and email for invite' + ) + return CollaboratorsEmailHandler.notifyUserOfProjectInvite( + projectId, + invite.email, + invite, + sendingUser, + function(err) { + if (err != null) { + return callback(err) + } + return CollaboratorsInviteHandler._trySendInviteNotification( + projectId, + sendingUser, + invite, + function(err) { + if (err != null) { + return callback(err) + } + return callback() + } + ) + } + ) + }, + + inviteToProject(projectId, sendingUser, email, privileges, callback) { + if (callback == null) { + callback = function(err, invite) {} + } + logger.log( + { projectId, sendingUserId: sendingUser._id, email, privileges }, + 'adding invite' + ) + return Crypto.randomBytes(24, function(err, buffer) { + if (err != null) { + logger.err( + { err, projectId, sendingUserId: sendingUser._id, email }, + 'error generating random token' + ) + return callback(err) + } + const token = buffer.toString('hex') + const invite = new ProjectInvite({ + email, + token, + sendingUserId: sendingUser._id, + projectId, + privileges + }) + return invite.save(function(err, invite) { + if (err != null) { + logger.err( + { err, projectId, sendingUserId: sendingUser._id, email }, + 'error saving token' + ) + return callback(err) + } + // Send email and notification in background + CollaboratorsInviteHandler._sendMessages( + projectId, + sendingUser, + invite, + function(err) { + if (err != null) { + return logger.err( + { err, projectId, email }, + 'error sending messages for invite' + ) + } + } + ) + return callback(null, invite) + }) + }) + }, + + revokeInvite(projectId, inviteId, callback) { + if (callback == null) { + callback = function(err) {} + } + logger.log({ projectId, inviteId }, 'removing invite') + return ProjectInvite.remove({ projectId, _id: inviteId }, function(err) { + if (err != null) { + logger.err({ err, projectId, inviteId }, 'error removing invite') + return callback(err) + } + CollaboratorsInviteHandler._tryCancelInviteNotification( + inviteId, + function() {} + ) + return callback(null) + }) + }, + + resendInvite(projectId, sendingUser, inviteId, callback) { + if (callback == null) { + callback = function(err) {} + } + logger.log({ projectId, inviteId }, 'resending invite email') + return ProjectInvite.findOne({ _id: inviteId, projectId }, function( + err, + invite + ) { + if (err != null) { + logger.err({ err, projectId, inviteId }, 'error finding invite') + return callback(err) + } + if (invite == null) { + logger.err( + { err, projectId, inviteId }, + 'no invite found, nothing to resend' + ) + return callback(null) + } + return CollaboratorsInviteHandler._sendMessages( + projectId, + sendingUser, + invite, + function(err) { + if (err != null) { + logger.err( + { projectId, inviteId }, + 'error resending invite messages' + ) + return callback(err) + } + return callback(null) + } + ) + }) + }, + + getInviteByToken(projectId, tokenString, callback) { + if (callback == null) { + callback = function(err, invite) {} + } + logger.log({ projectId, tokenString }, 'fetching invite by token') + return ProjectInvite.findOne({ projectId, token: tokenString }, function( + err, + invite + ) { + if (err != null) { + logger.err({ err, projectId }, 'error fetching invite') + return callback(err) + } + if (invite == null) { + logger.err({ err, projectId, token: tokenString }, 'no invite found') + return callback(null, null) + } + return callback(null, invite) + }) + }, + + acceptInvite(projectId, tokenString, user, callback) { + if (callback == null) { + callback = function(err) {} + } + logger.log({ projectId, userId: user._id, tokenString }, 'accepting invite') + return CollaboratorsInviteHandler.getInviteByToken( + projectId, + tokenString, + function(err, invite) { + if (err != null) { + logger.err({ err, projectId, tokenString }, 'error finding invite') + return callback(err) + } + if (!invite) { + err = new Errors.NotFoundError('no matching invite found') + logger.log( + { err, projectId, tokenString }, + 'no matching invite found' + ) + return callback(err) + } + const inviteId = invite._id + return CollaboratorsHandler.addUserIdToProject( + projectId, + invite.sendingUserId, + user._id, + invite.privileges, + function(err) { + if (err != null) { + logger.err( + { err, projectId, inviteId, userId: user._id }, + 'error adding user to project' + ) + return callback(err) + } + // Remove invite + logger.log({ projectId, inviteId }, 'removing invite') + return ProjectInvite.remove({ _id: inviteId }, function(err) { + if (err != null) { + logger.err( + { err, projectId, inviteId }, + 'error removing invite' + ) + return callback(err) + } + CollaboratorsInviteHandler._tryCancelInviteNotification( + inviteId, + function() {} + ) + return callback() + }) + } + ) + } + ) + } +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js b/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js new file mode 100644 index 0000000000..6b9889646d --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js @@ -0,0 +1,99 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const CollaboratorsController = require('./CollaboratorsController') +const AuthenticationController = require('../Authentication/AuthenticationController') +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const CollaboratorsInviteController = require('./CollaboratorsInviteController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const CaptchaMiddleware = require('../Captcha/CaptchaMiddleware') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.post( + '/project/:Project_id/leave', + AuthenticationController.requireLogin(), + CollaboratorsController.removeSelfFromProject + ) + + webRouter.delete( + '/project/:Project_id/users/:user_id', + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsController.removeUserFromProject + ) + + webRouter.get( + '/project/:Project_id/members', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsController.getAllMembers + ) + + // invites + webRouter.post( + '/project/:Project_id/invite', + RateLimiterMiddleware.rateLimit({ + endpointName: 'invite-to-project-by-project-id', + params: ['Project_id'], + maxRequests: 100, + timeInterval: 60 * 10 + }), + RateLimiterMiddleware.rateLimit({ + endpointName: 'invite-to-project-by-ip', + ipOnly: true, + maxRequests: 100, + timeInterval: 60 * 10 + }), + CaptchaMiddleware.validateCaptcha('invite'), + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.inviteToProject + ) + + webRouter.get( + '/project/:Project_id/invites', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.getAllInvites + ) + + webRouter.delete( + '/project/:Project_id/invite/:invite_id', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.revokeInvite + ) + + webRouter.post( + '/project/:Project_id/invite/:invite_id/resend', + RateLimiterMiddleware.rateLimit({ + endpointName: 'resend-invite', + params: ['Project_id'], + maxRequests: 200, + timeInterval: 60 * 10 + }), + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.resendInvite + ) + + webRouter.get( + '/project/:Project_id/invite/token/:token', + AuthenticationController.requireLogin(), + CollaboratorsInviteController.viewInvite + ) + + return webRouter.post( + '/project/:Project_id/invite/token/:token/accept', + AuthenticationController.requireLogin(), + CollaboratorsInviteController.acceptInvite + ) + } +} diff --git a/services/web/app/src/Features/Compile/ClsiCookieManager.js b/services/web/app/src/Features/Compile/ClsiCookieManager.js new file mode 100644 index 0000000000..ad120afcc2 --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiCookieManager.js @@ -0,0 +1,154 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let rclient_secondary +const Settings = require('settings-sharelatex') +const request = require('request') +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('clsi_cookie') +if (Settings.redis.clsi_cookie_secondary != null) { + rclient_secondary = RedisWrapper.client('clsi_cookie_secondary') +} +const Cookie = require('cookie') +const logger = require('logger-sharelatex') + +const clsiCookiesEnabled = + (Settings.clsiCookie != null ? Settings.clsiCookie.key : undefined) != null && + Settings.clsiCookie.key.length !== 0 + +module.exports = function(backendGroup) { + return { + buildKey(project_id) { + if (backendGroup != null) { + return `clsiserver:${backendGroup}:${project_id}` + } else { + return `clsiserver:${project_id}` + } + }, + + _getServerId(project_id, callback) { + if (callback == null) { + callback = function(err, serverId) {} + } + return rclient.get(this.buildKey(project_id), (err, serverId) => { + if (err != null) { + return callback(err) + } + if (serverId == null || serverId === '') { + return this._populateServerIdViaRequest(project_id, callback) + } else { + return callback(null, serverId) + } + }) + }, + + _populateServerIdViaRequest(project_id, callback) { + if (callback == null) { + callback = function(err, serverId) {} + } + const url = `${Settings.apis.clsi.url}/project/${project_id}/status` + return request.get(url, (err, res, body) => { + if (err != null) { + logger.err( + { err, project_id }, + 'error getting initial server id for project' + ) + return callback(err) + } + return this.setServerId(project_id, res, function(err, serverId) { + if (err != null) { + logger.err( + { err, project_id }, + 'error setting server id via populate request' + ) + } + return callback(err, serverId) + }) + }) + }, + + _parseServerIdFromResponse(response) { + const cookies = Cookie.parse( + (response.headers['set-cookie'] != null + ? response.headers['set-cookie'][0] + : undefined) || '' + ) + return cookies != null ? cookies[Settings.clsiCookie.key] : undefined + }, + + setServerId(project_id, response, callback) { + if (callback == null) { + callback = function(err, serverId) {} + } + if (!clsiCookiesEnabled) { + return callback() + } + const serverId = this._parseServerIdFromResponse(response) + if (serverId == null) { + // We don't get a cookie back if it hasn't changed + return rclient.expire( + this.buildKey(project_id), + Settings.clsiCookie.ttl, + callback + ) + } + if (rclient_secondary != null) { + this._setServerIdInRedis(rclient_secondary, project_id, serverId) + } + return this._setServerIdInRedis(rclient, project_id, serverId, err => + callback(err, serverId) + ) + }, + + _setServerIdInRedis(rclient, project_id, serverId, callback) { + if (callback == null) { + callback = function(err) {} + } + const multi = rclient.multi() + multi.set(this.buildKey(project_id), serverId) + multi.expire(this.buildKey(project_id), Settings.clsiCookie.ttl) + return multi.exec(callback) + }, + + clearServerId(project_id, callback) { + if (callback == null) { + callback = function(err) {} + } + if (!clsiCookiesEnabled) { + return callback() + } + return rclient.del(this.buildKey(project_id), callback) + }, + + getCookieJar(project_id, callback) { + if (callback == null) { + callback = function(err, jar) {} + } + if (!clsiCookiesEnabled) { + return callback(null, request.jar()) + } + return this._getServerId(project_id, (err, serverId) => { + if (err != null) { + logger.err({ err, project_id }, 'error getting server id') + return callback(err) + } + const serverCookie = request.cookie( + `${Settings.clsiCookie.key}=${serverId}` + ) + const jar = request.jar() + jar.setCookie(serverCookie, Settings.apis.clsi.url) + return callback(null, jar) + }) + } + } +} diff --git a/services/web/app/src/Features/Compile/ClsiFormatChecker.js b/services/web/app/src/Features/Compile/ClsiFormatChecker.js new file mode 100644 index 0000000000..dacded23c9 --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiFormatChecker.js @@ -0,0 +1,88 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ClsiFormatChecker +const _ = require('lodash') +const async = require('async') +const settings = require('settings-sharelatex') + +module.exports = ClsiFormatChecker = { + checkRecoursesForProblems(resources, callback) { + const jobs = { + conflictedPaths(cb) { + return ClsiFormatChecker._checkForConflictingPaths(resources, cb) + }, + + sizeCheck(cb) { + return ClsiFormatChecker._checkDocsAreUnderSizeLimit(resources, cb) + } + } + + return async.series(jobs, function(err, problems) { + if (err != null) { + return callback(err) + } + + problems = _.omitBy(problems, _.isEmpty) + + if (_.isEmpty(problems)) { + return callback() + } else { + return callback(null, problems) + } + }) + }, + + _checkForConflictingPaths(resources, callback) { + const paths = _.map(resources, 'path') + + const conflicts = _.filter(paths, function(path) { + const matchingPaths = _.filter( + paths, + checkPath => checkPath.indexOf(path + '/') !== -1 + ) + + return matchingPaths.length > 0 + }) + + const conflictObjects = _.map(conflicts, conflict => ({ path: conflict })) + + return callback(null, conflictObjects) + }, + + _checkDocsAreUnderSizeLimit(resources, callback) { + const sizeLimit = 1000 * 1000 * settings.compileBodySizeLimitMb + + let totalSize = 0 + + let sizedResources = _.map(resources, function(resource) { + const result = { path: resource.path } + if (resource.content != null) { + result.size = resource.content.replace(/\n/g).length + result.kbSize = Math.ceil(result.size / 1000) + } else { + result.size = 0 + } + totalSize += result.size + return result + }) + + const tooLarge = totalSize > sizeLimit + if (!tooLarge) { + return callback() + } else { + sizedResources = _.sortBy(sizedResources, 'size') + .reverse() + .slice(0, 10) + return callback(null, { resources: sizedResources, totalSize }) + } + } +} diff --git a/services/web/app/src/Features/Compile/ClsiManager.js b/services/web/app/src/Features/Compile/ClsiManager.js new file mode 100644 index 0000000000..0aea76bc4e --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiManager.js @@ -0,0 +1,922 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ClsiManager +const Path = require('path') +let async = require('async') +const Settings = require('settings-sharelatex') +const request = require('request') +const { Project } = require('../../models/Project') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const logger = require('logger-sharelatex') +const Url = require('url') +const ClsiCookieManager = require('./ClsiCookieManager')( + Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined +) +const NewBackendCloudClsiCookieManager = require('./ClsiCookieManager')( + Settings.apis.clsi_new != null + ? Settings.apis.clsi_new.backendGroupName + : undefined +) +const ClsiStateManager = require('./ClsiStateManager') +const _ = require('underscore') +async = require('async') +const ClsiFormatChecker = require('./ClsiFormatChecker') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const Metrics = require('metrics-sharelatex') +const Errors = require('../Errors/Errors') + +module.exports = ClsiManager = { + sendRequest(project_id, user_id, options, callback) { + if (options == null) { + options = {} + } + return ClsiManager.sendRequestOnce(project_id, user_id, options, function( + error, + status, + ...result + ) { + if (error != null) { + return callback(error) + } + if (status === 'conflict') { + options = _.clone(options) + options.syncType = 'full' // force full compile + return ClsiManager.sendRequestOnce( + project_id, + user_id, + options, + callback + ) // try again + } else { + return callback(error, status, ...Array.from(result)) + } + }) + }, + + sendRequestOnce(project_id, user_id, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function( + error, + status, + outputFiles, + clsiServerId, + validationProblems + ) {} + } + return ClsiManager._buildRequest(project_id, options, function(error, req) { + if (error != null) { + if (error.message === 'no main file specified') { + return callback(null, 'validation-problems', null, null, { + mainFile: error.message + }) + } else { + return callback(error) + } + } + logger.log({ project_id }, 'sending compile to CLSI') + return ClsiManager._sendBuiltRequest( + project_id, + user_id, + req, + options, + function(error, status, ...result) { + if (error != null) { + return callback(error) + } + return callback(error, status, ...Array.from(result)) + } + ) + }) + }, + + // for public API requests where there is no project id + sendExternalRequest(submission_id, clsi_request, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function( + error, + status, + outputFiles, + clsiServerId, + validationProblems + ) {} + } + logger.log( + { submission_id }, + 'sending external compile to CLSI', + clsi_request + ) + return ClsiManager._sendBuiltRequest( + submission_id, + null, + clsi_request, + options, + function(error, status, ...result) { + if (error != null) { + return callback(error) + } + return callback(error, status, ...Array.from(result)) + } + ) + }, + + stopCompile(project_id, user_id, options, callback) { + if (callback == null) { + callback = function(error) {} + } + const compilerUrl = this._getCompilerUrl( + options != null ? options.compileGroup : undefined, + project_id, + user_id, + 'compile/stop' + ) + const opts = { + url: compilerUrl, + method: 'POST' + } + return ClsiManager._makeRequest(project_id, opts, callback) + }, + + deleteAuxFiles(project_id, user_id, options, callback) { + if (callback == null) { + callback = function(error) {} + } + const compilerUrl = this._getCompilerUrl( + options != null ? options.compileGroup : undefined, + project_id, + user_id + ) + const opts = { + url: compilerUrl, + method: 'DELETE' + } + return ClsiManager._makeRequest(project_id, opts, clsiError => + // always clear the project state from the docupdater, even if there + // was a problem with the request to the clsi + DocumentUpdaterHandler.clearProjectState(project_id, function( + docUpdaterError + ) { + const error = clsiError || docUpdaterError + if (error != null) { + return callback(error) + } + return callback() + }) + ) + }, + + _sendBuiltRequest(project_id, user_id, req, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function( + error, + status, + outputFiles, + clsiServerId, + validationProblems + ) {} + } + return ClsiFormatChecker.checkRecoursesForProblems( + req.compile != null ? req.compile.resources : undefined, + function(err, validationProblems) { + if (err != null) { + logger.err( + err, + project_id, + 'could not check resources for potential problems before sending to clsi' + ) + return callback(err) + } + if (validationProblems != null) { + logger.log( + { project_id, validationProblems }, + 'problems with users latex before compile was attempted' + ) + return callback( + null, + 'validation-problems', + null, + null, + validationProblems + ) + } + return ClsiManager._postToClsi( + project_id, + user_id, + req, + options.compileGroup, + function(error, response) { + if (error != null) { + logger.err( + { err: error, project_id }, + 'error sending request to clsi' + ) + return callback(error) + } + logger.log( + { + project_id, + outputFilesLength: __guard__( + response != null ? response.outputFiles : undefined, + x => x.length + ), + status: response != null ? response.status : undefined, + compile_status: __guard__( + response != null ? response.compile : undefined, + x1 => x1.status + ) + }, + 'received compile response from CLSI' + ) + return ClsiCookieManager._getServerId(project_id, function( + err, + clsiServerId + ) { + if (err != null) { + logger.err({ err, project_id }, 'error getting server id') + return callback(err) + } + const outputFiles = ClsiManager._parseOutputFiles( + project_id, + __guard__( + response != null ? response.compile : undefined, + x2 => x2.outputFiles + ) + ) + return callback( + null, + __guard__( + response != null ? response.compile : undefined, + x3 => x3.status + ), + outputFiles, + clsiServerId + ) + }) + } + ) + } + ) + }, + + _makeRequest(project_id, opts, callback) { + return async.series( + { + currentBackend(cb) { + const startTime = new Date() + return ClsiCookieManager.getCookieJar(project_id, function(err, jar) { + if (err != null) { + logger.err({ err }, 'error getting cookie jar for clsi request') + return callback(err) + } + opts.jar = jar + const timer = new Metrics.Timer('compile.currentBackend') + return request(opts, function(err, response, body) { + timer.done() + Metrics.inc( + `compile.currentBackend.response.${ + response != null ? response.statusCode : undefined + }` + ) + if (err != null) { + logger.err( + { err, project_id, url: opts != null ? opts.url : undefined }, + 'error making request to clsi' + ) + return callback(err) + } + return ClsiCookieManager.setServerId( + project_id, + response, + function(err) { + if (err != null) { + logger.warn({ err, project_id }, 'error setting server id') + } + callback(err, response, body) // return as soon as the standard compile has returned + return cb(err, { + response, + body, + finishTime: new Date() - startTime + }) + } + ) + }) + }) + }, + newBackend(cb) { + const startTime = new Date() + return ClsiManager._makeNewBackendRequest(project_id, opts, function( + err, + response, + body + ) { + Metrics.inc( + `compile.newBackend.response.${ + response != null ? response.statusCode : undefined + }` + ) + return cb(err, { + response, + body, + finishTime: new Date() - startTime + }) + }) + } + }, + function(err, results) { + const timeDifference = + (results.newBackend != null + ? results.newBackend.finishTime + : undefined) - + (results.currentBackend != null + ? results.currentBackend.finishTime + : undefined) + const statusCodeSame = + __guard__( + results.newBackend != null + ? results.newBackend.response + : undefined, + x => x.statusCode + ) === + __guard__( + results.currentBackend != null + ? results.currentBackend.response + : undefined, + x1 => x1.statusCode + ) + const currentCompileTime = + results.currentBackend != null + ? results.currentBackend.finishTime + : undefined + const newBackendCompileTime = + results.newBackend != null ? results.newBackend.finishTime : undefined + return logger.log( + { + statusCodeSame, + timeDifference, + currentCompileTime, + newBackendCompileTime, + project_id + }, + 'both clsi requests returned' + ) + } + ) + }, + + _makeNewBackendRequest(project_id, baseOpts, callback) { + if ( + (Settings.apis.clsi_new != null + ? Settings.apis.clsi_new.url + : undefined) == null + ) { + return callback() + } + const opts = _.clone(baseOpts) + opts.url = opts.url.replace( + Settings.apis.clsi.url, + Settings.apis.clsi_new != null ? Settings.apis.clsi_new.url : undefined + ) + return NewBackendCloudClsiCookieManager.getCookieJar(project_id, function( + err, + jar + ) { + if (err != null) { + logger.err({ err }, 'error getting cookie jar for clsi request') + return callback(err) + } + opts.jar = jar + const timer = new Metrics.Timer('compile.newBackend') + return request(opts, function(err, response, body) { + timer.done() + if (err != null) { + logger.warn( + { err, project_id, url: opts != null ? opts.url : undefined }, + 'error making request to new clsi' + ) + return callback(err) + } + return NewBackendCloudClsiCookieManager.setServerId( + project_id, + response, + function(err) { + if (err != null) { + logger.warn( + { err, project_id }, + 'error setting server id new backend' + ) + } + return callback(err, response, body) + } + ) + }) + }) + }, + + _getCompilerUrl(compileGroup, project_id, user_id, action) { + const host = Settings.apis.clsi.url + let path = `/project/${project_id}` + if (user_id != null) { + path += `/user/${user_id}` + } + if (action != null) { + path += `/${action}` + } + return `${host}${path}` + }, + + _postToClsi(project_id, user_id, req, compileGroup, callback) { + if (callback == null) { + callback = function(error, response) {} + } + const compileUrl = this._getCompilerUrl( + compileGroup, + project_id, + user_id, + 'compile' + ) + const opts = { + url: compileUrl, + json: req, + method: 'POST' + } + return ClsiManager._makeRequest(project_id, opts, function( + error, + response, + body + ) { + if (error != null) { + return callback(error) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + return callback(null, body) + } else if (response.statusCode === 413) { + return callback(null, { compile: { status: 'project-too-large' } }) + } else if (response.statusCode === 409) { + return callback(null, { compile: { status: 'conflict' } }) + } else if (response.statusCode === 423) { + return callback(null, { compile: { status: 'compile-in-progress' } }) + } else { + error = new Error( + `CLSI returned non-success code: ${response.statusCode}` + ) + logger.error({ err: error, project_id }, 'CLSI returned failure code') + return callback(error, body) + } + }) + }, + + _parseOutputFiles(project_id, rawOutputFiles) { + if (rawOutputFiles == null) { + rawOutputFiles = [] + } + const outputFiles = [] + for (let file of Array.from(rawOutputFiles)) { + outputFiles.push({ + 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 + }, + + VALID_COMPILERS: ['pdflatex', 'latex', 'xelatex', 'lualatex'], + + _buildRequest(project_id, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function(error, request) {} + } + return ProjectGetter.getProject( + project_id, + { compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder: 1 }, + function(error, project) { + let timer + if (error != null) { + return callback(error) + } + if (project == null) { + return callback( + new Errors.NotFoundError(`project does not exist: ${project_id}`) + ) + } + if ( + !Array.from(ClsiManager.VALID_COMPILERS).includes(project.compiler) + ) { + project.compiler = 'pdflatex' + } + + if (options.incrementalCompilesEnabled || options.syncType != null) { + // new way, either incremental or full + timer = new Metrics.Timer('editor.compile-getdocs-redis') + return ClsiManager.getContentFromDocUpdaterIfMatch( + project_id, + project, + options, + function(error, projectStateHash, docUpdaterDocs) { + timer.done() + if (error != null) { + logger.error( + { err: error, project_id }, + 'error checking project state' + ) + // note: we don't bail out when there's an error getting + // incremental files from the docupdater, we just fall back + // to a normal compile below + } else { + logger.log( + { + project_id, + projectStateHash, + docs: docUpdaterDocs != null + }, + 'checked project state' + ) + } + // see if we can send an incremental update to the CLSI + if ( + docUpdaterDocs != null && + options.syncType !== 'full' && + error == null + ) { + Metrics.inc('compile-from-redis') + return ClsiManager._buildRequestFromDocupdater( + project_id, + options, + project, + projectStateHash, + docUpdaterDocs, + callback + ) + } else { + Metrics.inc('compile-from-mongo') + return ClsiManager._buildRequestFromMongo( + project_id, + options, + project, + projectStateHash, + callback + ) + } + } + ) + } else { + // old way, always from mongo + timer = new Metrics.Timer('editor.compile-getdocs-mongo') + return ClsiManager._getContentFromMongo(project_id, function( + error, + docs, + files + ) { + timer.done() + if (error != null) { + return callback(error) + } + return ClsiManager._finaliseRequest( + project_id, + options, + project, + docs, + files, + callback + ) + }) + } + } + ) + }, + + getContentFromDocUpdaterIfMatch(project_id, project, options, callback) { + if (callback == null) { + callback = function(error, projectStateHash, docs) {} + } + return ClsiStateManager.computeHash(project, options, function( + error, + projectStateHash + ) { + if (error != null) { + return callback(error) + } + return DocumentUpdaterHandler.getProjectDocsIfMatch( + project_id, + projectStateHash, + function(error, docs) { + if (error != null) { + return callback(error) + } + return callback(null, projectStateHash, docs) + } + ) + }) + }, + + getOutputFileStream( + project_id, + user_id, + build_id, + output_file_path, + callback + ) { + if (callback == null) { + callback = function(err, readStream) {} + } + const url = `${ + Settings.apis.clsi.url + }/project/${project_id}/user/${user_id}/build/${build_id}/output/${output_file_path}` + return ClsiCookieManager.getCookieJar(project_id, function(err, jar) { + if (err != null) { + return callback(err) + } + const options = { url, method: 'GET', timeout: 60 * 1000, jar } + const readStream = request(options) + return callback(null, readStream) + }) + }, + + _buildRequestFromDocupdater( + project_id, + options, + project, + projectStateHash, + docUpdaterDocs, + callback + ) { + if (callback == null) { + callback = function(error, request) {} + } + return ProjectEntityHandler.getAllDocPathsFromProject(project, function( + error, + docPath + ) { + let path + if (error != null) { + return callback(error) + } + const docs = {} + for (let doc of Array.from(docUpdaterDocs || [])) { + path = docPath[doc._id] + docs[path] = doc + } + // send new docs but not files as those are already on the clsi + options = _.clone(options) + options.syncType = 'incremental' + options.syncState = projectStateHash + // create stub doc entries for any possible root docs, if not + // present in the docupdater. This allows finaliseRequest to + // identify the root doc. + const possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id] + for (let rootDoc_id of Array.from(possibleRootDocIds)) { + if (rootDoc_id != null && rootDoc_id in docPath) { + path = docPath[rootDoc_id] + if (docs[path] == null) { + docs[path] = { _id: rootDoc_id, path } + } + } + } + return ClsiManager._finaliseRequest( + project_id, + options, + project, + docs, + [], + callback + ) + }) + }, + + _buildRequestFromMongo( + project_id, + options, + project, + projectStateHash, + callback + ) { + if (callback == null) { + callback = function(error, request) {} + } + return ClsiManager._getContentFromMongo(project_id, function( + error, + docs, + files + ) { + if (error != null) { + return callback(error) + } + options = _.clone(options) + options.syncType = 'full' + options.syncState = projectStateHash + return ClsiManager._finaliseRequest( + project_id, + options, + project, + docs, + files, + callback + ) + }) + }, + + _getContentFromMongo(project_id, callback) { + if (callback == null) { + callback = function(error, docs, files) {} + } + return DocumentUpdaterHandler.flushProjectToMongo(project_id, function( + error + ) { + if (error != null) { + return callback(error) + } + return ProjectEntityHandler.getAllDocs(project_id, function(error, docs) { + if (docs == null) { + docs = {} + } + if (error != null) { + return callback(error) + } + return ProjectEntityHandler.getAllFiles(project_id, function( + error, + files + ) { + if (files == null) { + files = {} + } + if (error != null) { + return callback(error) + } + return callback(null, docs, files) + }) + }) + }) + }, + + _finaliseRequest(project_id, options, project, docs, files, callback) { + let doc, path + if (callback == null) { + callback = function(error, params) {} + } + const resources = [] + let rootResourcePath = null + let rootResourcePathOverride = null + let hasMainFile = false + let numberOfDocsInProject = 0 + + for (path in docs) { + doc = docs[path] + path = path.replace(/^\//, '') // Remove leading / + numberOfDocsInProject++ + if (doc.lines != null) { + // add doc to resources unless it is just a stub entry + resources.push({ + path, + content: doc.lines.join('\n') + }) + } + if ( + project.rootDoc_id != null && + doc._id.toString() === project.rootDoc_id.toString() + ) { + rootResourcePath = path + } + if ( + options.rootDoc_id != null && + doc._id.toString() === options.rootDoc_id.toString() + ) { + rootResourcePathOverride = path + } + if (path === 'main.tex') { + hasMainFile = true + } + } + + if (rootResourcePathOverride != null) { + rootResourcePath = rootResourcePathOverride + } + if (rootResourcePath == null) { + if (hasMainFile) { + logger.warn( + { project_id }, + 'no root document found, setting to main.tex' + ) + rootResourcePath = 'main.tex' + } else if (numberOfDocsInProject === 1) { + // only one file, must be the main document + for (path in docs) { + doc = docs[path] + rootResourcePath = path.replace(/^\//, '') + } // Remove leading / + logger.warn( + { project_id, rootResourcePath }, + 'no root document found, single document in project' + ) + } else { + return callback(new Error('no main file specified')) + } + } + + for (path in files) { + const file = files[path] + path = path.replace(/^\//, '') // Remove leading / + resources.push({ + path, + url: `${Settings.apis.filestore.url}/project/${project._id}/file/${ + file._id + }`, + modified: file.created != null ? file.created.getTime() : undefined + }) + } + + return callback(null, { + compile: { + options: { + compiler: project.compiler, + timeout: options.timeout, + imageName: project.imageName, + draft: !!options.draft, + check: options.check, + syncType: options.syncType, + syncState: options.syncState + }, + rootResourcePath, + resources + } + }) + }, + + wordCount(project_id, user_id, file, options, callback) { + if (callback == null) { + callback = function(error, response) {} + } + return ClsiManager._buildRequest(project_id, options, function(error, req) { + const filename = + file || + __guard__( + req != null ? req.compile : undefined, + x => x.rootResourcePath + ) + const wordcount_url = ClsiManager._getCompilerUrl( + options != null ? options.compileGroup : undefined, + project_id, + user_id, + 'wordcount' + ) + const opts = { + url: wordcount_url, + qs: { + file: filename, + image: req.compile.options.imageName + }, + method: 'GET' + } + return ClsiManager._makeRequest(project_id, opts, function( + error, + response, + body + ) { + if (error != null) { + return callback(error) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + return callback(null, body) + } else { + error = new Error( + `CLSI returned non-success code: ${response.statusCode}` + ) + logger.error({ err: error, project_id }, 'CLSI returned failure code') + return callback(error, body) + } + }) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Compile/ClsiStateManager.js b/services/web/app/src/Features/Compile/ClsiStateManager.js new file mode 100644 index 0000000000..d65bc9a73e --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiStateManager.js @@ -0,0 +1,83 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ClsiStateManager +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const crypto = require('crypto') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') + +// The "state" of a project is a hash of the relevant attributes in the +// project object in this case we only need the rootFolder. +// +// The idea is that it will change if any doc or file is +// created/renamed/deleted, and also if the content of any file (not +// doc) changes. +// +// When the hash changes the full set of files on the CLSI will need to +// be updated. If it doesn't change then we can overwrite changed docs +// in place on the clsi, getting them from the docupdater. +// +// The docupdater is responsible for setting the key in redis, and +// unsetting it if it removes any documents from the doc updater. + +const buildState = s => + crypto + .createHash('sha1') + .update(s, 'utf8') + .digest('hex') + +module.exports = ClsiStateManager = { + computeHash(project, options, callback) { + if (callback == null) { + callback = function(err, hash) {} + } + return ProjectEntityHandler.getAllEntitiesFromProject(project, function( + err, + docs, + files + ) { + const fileList = Array.from(files || []).map( + f => `${f.file._id}:${f.file.rev}:${f.file.created}:${f.path}` + ) + const docList = Array.from(docs || []).map(d => `${d.doc._id}:${d.path}`) + const sortedEntityList = [ + ...Array.from(docList), + ...Array.from(fileList) + ].sort() + // ignore the isAutoCompile options as it doesn't affect the + // output, but include all other options e.g. draft + const optionsList = (() => { + const result = [] + const object = options || {} + for (let key in object) { + const value = object[key] + if (!['isAutoCompile'].includes(key)) { + result.push(`option ${key}:${value}`) + } + } + return result + })() + const sortedOptionsList = optionsList.sort() + const hash = buildState( + [ + ...Array.from(sortedEntityList), + ...Array.from(sortedOptionsList) + ].join('\n') + ) + return callback(null, hash) + }) + } +} diff --git a/services/web/app/src/Features/Compile/CompileController.js b/services/web/app/src/Features/Compile/CompileController.js new file mode 100644 index 0000000000..e81a110078 --- /dev/null +++ b/services/web/app/src/Features/Compile/CompileController.js @@ -0,0 +1,556 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CompileController +const Metrics = require('metrics-sharelatex') +const ProjectGetter = require('../Project/ProjectGetter') +const CompileManager = require('./CompileManager') +const ClsiManager = require('./ClsiManager') +const logger = require('logger-sharelatex') +const request = require('request') +const sanitize = require('sanitizer') +const Settings = require('settings-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') +const UserGetter = require('../User/UserGetter') +const RateLimiter = require('../../infrastructure/RateLimiter') +const ClsiCookieManager = require('./ClsiCookieManager')( + Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined +) +const Path = require('path') + +module.exports = CompileController = { + compile(req, res, next) { + if (next == null) { + next = function(error) {} + } + res.setTimeout(5 * 60 * 1000) + const project_id = req.params.Project_id + const isAutoCompile = !!(req.query != null + ? req.query.auto_compile + : undefined) + const user_id = AuthenticationController.getLoggedInUserId(req) + const options = { + isAutoCompile + } + if ((req.body != null ? req.body.rootDoc_id : undefined) != null) { + options.rootDoc_id = req.body.rootDoc_id + } else if ( + __guard__( + req.body != null ? req.body.settingsOverride : undefined, + x => x.rootDoc_id + ) != null + ) { + // Can be removed after deploy + options.rootDoc_id = req.body.settingsOverride.rootDoc_id + } + if (req.body != null ? req.body.compiler : undefined) { + options.compiler = req.body.compiler + } + if (req.body != null ? req.body.draft : undefined) { + options.draft = req.body.draft + } + if ( + ['validate', 'error', 'silent'].includes( + req.body != null ? req.body.check : undefined + ) + ) { + options.check = req.body.check + } + if (req.body != null ? req.body.incrementalCompilesEnabled : undefined) { + options.incrementalCompilesEnabled = true + } + logger.log({ options, project_id, user_id }, 'got compile request') + return CompileManager.compile(project_id, user_id, options, function( + error, + status, + outputFiles, + clsiServerId, + limits, + validationProblems + ) { + if (error != null) { + return next(error) + } + res.contentType('application/json') + return res.status(200).send( + JSON.stringify({ + status, + outputFiles, + compileGroup: limits != null ? limits.compileGroup : undefined, + clsiServerId, + validationProblems, + pdfDownloadDomain: Settings.pdfDownloadDomain + }) + ) + }) + }, + + stopCompile(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ project_id, user_id }, 'stop compile request') + return CompileManager.stopCompile(project_id, user_id, function(error) { + if (error != null) { + return next(error) + } + return res.status(200).send() + }) + }, + + // Used for submissions through the public API + compileSubmission(req, res, next) { + if (next == null) { + next = function(error) {} + } + res.setTimeout(5 * 60 * 1000) + const { submission_id } = req.params + const options = {} + if ((req.body != null ? req.body.rootResourcePath : undefined) != null) { + options.rootResourcePath = req.body.rootResourcePath + } + if (req.body != null ? req.body.compiler : undefined) { + options.compiler = req.body.compiler + } + if (req.body != null ? req.body.draft : undefined) { + options.draft = req.body.draft + } + if ( + ['validate', 'error', 'silent'].includes( + req.body != null ? req.body.check : undefined + ) + ) { + options.check = req.body.check + } + options.compileGroup = + (req.body != null ? req.body.compileGroup : undefined) || + Settings.defaultFeatures.compileGroup + options.timeout = + (req.body != null ? req.body.timeout : undefined) || + Settings.defaultFeatures.compileTimeout + logger.log({ options, submission_id }, 'got compileSubmission request') + return ClsiManager.sendExternalRequest( + submission_id, + req.body, + options, + function(error, status, outputFiles, clsiServerId, validationProblems) { + if (error != null) { + return next(error) + } + logger.log( + { submission_id, files: outputFiles }, + 'compileSubmission output files' + ) + res.contentType('application/json') + return res.status(200).send( + JSON.stringify({ + status, + outputFiles, + clsiServerId, + validationProblems + }) + ) + } + ) + }, + + _compileAsUser(req, callback) { + // callback with user_id if per-user, undefined otherwise + if (!Settings.disablePerUserCompiles) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return callback(null, user_id) + } else { + return callback() + } + }, // do a per-project compile, not per-user + + _downloadAsUser(req, callback) { + // callback with user_id if per-user, undefined otherwise + if (!Settings.disablePerUserCompiles) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return callback(null, user_id) + } else { + return callback() + } + }, // do a per-project compile, not per-user + + downloadPdf(req, res, next) { + if (next == null) { + next = function(error) {} + } + Metrics.inc('pdf-downloads') + const project_id = req.params.Project_id + const isPdfjsPartialDownload = + req.query != null ? req.query.pdfng : undefined + const rateLimit = function(callback) { + if (isPdfjsPartialDownload) { + return callback(null, true) + } else { + const rateLimitOpts = { + endpointName: 'full-pdf-download', + throttle: 1000, + subjectName: req.ip, + timeInterval: 60 * 60 + } + return RateLimiter.addCount(rateLimitOpts, callback) + } + } + + return ProjectGetter.getProject(project_id, { name: 1 }, function( + err, + project + ) { + res.contentType('application/pdf') + const filename = `${CompileController._getSafeProjectName(project)}.pdf` + + if (req.query.popupDownload) { + logger.log({ project_id }, 'download pdf as popup download') + res.setContentDisposition('attachment', { filename }) + } else { + logger.log({ project_id }, 'download pdf to embed in browser') + res.setContentDisposition('', { filename }) + } + + return rateLimit(function(err, canContinue) { + if (err != null) { + logger.err({ err }, 'error checking rate limit for pdf download') + return res.send(500) + } else if (!canContinue) { + logger.log( + { project_id, ip: req.ip }, + 'rate limit hit downloading pdf' + ) + return res.send(500) + } else { + return CompileController._downloadAsUser(req, function( + error, + user_id + ) { + const url = CompileController._getFileUrl( + project_id, + user_id, + req.params.build_id, + 'output.pdf' + ) + return CompileController.proxyToClsi( + project_id, + url, + req, + res, + next + ) + }) + } + }) + }) + }, + + _getSafeProjectName(project) { + const safeProjectName = project.name.replace(new RegExp('\\W', 'g'), '_') + return sanitize.escape(safeProjectName) + }, + + deleteAuxFiles(req, res, next) { + const project_id = req.params.Project_id + return CompileController._compileAsUser(req, function(error, user_id) { + if (error != null) { + return next(error) + } + return CompileManager.deleteAuxFiles(project_id, user_id, function( + error + ) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + }) + }, + + // this is only used by templates, so is not called with a user_id + compileAndDownloadPdf(req, res, next) { + const { project_id } = req.params + // pass user_id as null, since templates are an "anonymous" compile + return CompileManager.compile(project_id, null, {}, function(err) { + if (err != null) { + logger.err( + { err, project_id }, + 'something went wrong compile and downloading pdf' + ) + res.sendStatus(500) + } + const url = `/project/${project_id}/output/output.pdf` + return CompileController.proxyToClsi(project_id, url, req, res, next) + }) + }, + + getFileFromClsi(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + return CompileController._downloadAsUser(req, function(error, user_id) { + if (error != null) { + return next(error) + } + const url = CompileController._getFileUrl( + project_id, + user_id, + req.params.build_id, + req.params.file + ) + return CompileController.proxyToClsi(project_id, url, req, res, next) + }) + }, + + getFileFromClsiWithoutUser(req, res, next) { + if (next == null) { + next = function(error) {} + } + const { submission_id } = req.params + const url = CompileController._getFileUrl( + submission_id, + null, + req.params.build_id, + req.params.file + ) + const limits = { + compileGroup: + (req.body != null ? req.body.compileGroup : undefined) || + Settings.defaultFeatures.compileGroup + } + return CompileController.proxyToClsiWithLimits( + submission_id, + url, + limits, + 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) { + let url + if (user_id != null && build_id != null) { + url = `/project/${project_id}/user/${user_id}/build/${build_id}/output/${file}` + } else if (user_id != null) { + url = `/project/${project_id}/user/${user_id}/output/${file}` + } else if (build_id != null) { + 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) { + let path = `/project/${project_id}` + if (user_id != null) { + path += `/user/${user_id}` + } + return `${path}/${action}` + }, + + proxySyncPdf(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + const { page, h, v } = req.query + if (!(page != null ? page.match(/^\d+$/) : undefined)) { + return next(new Error('invalid page parameter')) + } + if (!(h != null ? h.match(/^-?\d+\.\d+$/) : undefined)) { + return next(new Error('invalid h parameter')) + } + if (!(v != null ? v.match(/^-?\d+\.\d+$/) : undefined)) { + return next(new Error('invalid v parameter')) + } + // whether this request is going to a per-user container + return CompileController._compileAsUser(req, function(error, user_id) { + if (error != null) { + return next(error) + } + const url = CompileController._getUrl(project_id, user_id, 'sync/pdf') + const destination = { url, qs: { page, h, v } } + return CompileController.proxyToClsi( + project_id, + destination, + req, + res, + next + ) + }) + }, + + proxySyncCode(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + const { file, line, column } = req.query + if (file == null) { + 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 / + const testPath = file.replace('/./', '/') + if (Path.resolve('/', testPath) !== `/${testPath}`) { + return next(new Error('invalid file parameter')) + } + if (!(line != null ? line.match(/^\d+$/) : undefined)) { + return next(new Error('invalid line parameter')) + } + if (!(column != null ? column.match(/^\d+$/) : undefined)) { + return next(new Error('invalid column parameter')) + } + return CompileController._compileAsUser(req, function(error, user_id) { + if (error != null) { + return next(error) + } + const url = CompileController._getUrl(project_id, user_id, 'sync/code') + const destination = { url, qs: { file, line, column } } + return CompileController.proxyToClsi( + project_id, + destination, + req, + res, + next + ) + }) + }, + + proxyToClsi(project_id, url, req, res, next) { + if (next == null) { + next = function(error) {} + } + if (req.query != null ? req.query.compileGroup : undefined) { + return CompileController.proxyToClsiWithLimits( + project_id, + url, + { compileGroup: req.query.compileGroup }, + req, + res, + next + ) + } else { + return CompileManager.getProjectCompileLimits(project_id, function( + error, + limits + ) { + if (error != null) { + return next(error) + } + return CompileController.proxyToClsiWithLimits( + project_id, + url, + limits, + req, + res, + next + ) + }) + } + }, + + proxyToClsiWithLimits(project_id, url, limits, req, res, next) { + if (next == null) { + next = function(error) {} + } + return ClsiCookieManager.getCookieJar(project_id, function(err, jar) { + let qs + if (err != null) { + logger.err({ err }, 'error getting cookie jar for clsi request') + return callback(err) + } + // expand any url parameter passed in as {url:..., qs:...} + if (typeof url === 'object') { + ;({ url, qs } = url) + } + const compilerUrl = Settings.apis.clsi.url + url = `${compilerUrl}${url}` + logger.log({ url }, 'proxying to CLSI') + const oneMinute = 60 * 1000 + // the base request + const options = { url, method: req.method, timeout: oneMinute, jar } + // add any provided query string + if (qs != null) { + options.qs = qs + } + // if we have a build parameter, pass it through to the clsi + if ( + (req.query != null ? req.query.pdfng : undefined) && + (req.query != null ? req.query.build : undefined) != null + ) { + // only for new pdf viewer + if (options.qs == null) { + options.qs = {} + } + options.qs.build = req.query.build + } + // if we are byte serving pdfs, pass through If-* and Range headers + // do not send any others, there's a proxying loop if Host: is passed! + if (req.query != null ? req.query.pdfng : undefined) { + const newHeaders = {} + for (let h in req.headers) { + const v = req.headers[h] + if (/^(If-|Range)/i.test(h)) { + newHeaders[h] = req.headers[h] + } + } + options.headers = newHeaders + } + const proxy = request(options) + proxy.pipe(res) + return proxy.on('error', error => + logger.warn({ err: error, url }, 'CLSI proxy error') + ) + }) + }, + + wordCount(req, res, next) { + const project_id = req.params.Project_id + const file = req.query.file || false + return CompileController._compileAsUser(req, function(error, user_id) { + if (error != null) { + return next(error) + } + return CompileManager.wordCount(project_id, user_id, file, function( + error, + body + ) { + if (error != null) { + return next(error) + } + res.contentType('application/json') + return res.send(body) + }) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Compile/CompileManager.js b/services/web/app/src/Features/Compile/CompileManager.js new file mode 100644 index 0000000000..ecc21e55f4 --- /dev/null +++ b/services/web/app/src/Features/Compile/CompileManager.js @@ -0,0 +1,288 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CompileManager +const Settings = require('settings-sharelatex') +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('clsi_recently_compiled') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const UserGetter = require('../User/UserGetter') +const ClsiManager = require('./ClsiManager') +const Metrics = require('metrics-sharelatex') +const logger = require('logger-sharelatex') +const rateLimiter = require('../../infrastructure/RateLimiter') + +module.exports = CompileManager = { + compile(project_id, user_id, options, _callback) { + if (options == null) { + options = {} + } + if (_callback == null) { + _callback = function(error) {} + } + const timer = new Metrics.Timer('editor.compile') + const callback = function(...args) { + timer.done() + return _callback(...Array.from(args || [])) + } + + logger.log({ project_id, user_id }, 'compiling project') + return CompileManager._checkIfRecentlyCompiled( + project_id, + user_id, + function(error, recentlyCompiled) { + if (error != null) { + return callback(error) + } + if (recentlyCompiled) { + logger.warn( + { project_id, user_id }, + 'project was recently compiled so not continuing' + ) + return callback(null, 'too-recently-compiled', []) + } + + return CompileManager._checkIfAutoCompileLimitHasBeenHit( + options.isAutoCompile, + 'everyone', + function(err, canCompile) { + if (!canCompile) { + return callback(null, 'autocompile-backoff', []) + } + + return ProjectRootDocManager.ensureRootDocumentIsSet( + project_id, + function(error) { + if (error != null) { + return callback(error) + } + return CompileManager.getProjectCompileLimits( + project_id, + function(error, limits) { + if (error != null) { + return callback(error) + } + for (let key in limits) { + const value = limits[key] + options[key] = value + } + // Put a lower limit on autocompiles for free users, based on compileGroup + return CompileManager._checkCompileGroupAutoCompileLimit( + options.isAutoCompile, + limits.compileGroup, + function(err, canCompile) { + if (!canCompile) { + return callback(null, 'autocompile-backoff', []) + } + // only pass user_id down to clsi if this is a per-user compile + const compileAsUser = Settings.disablePerUserCompiles + ? undefined + : user_id + return ClsiManager.sendRequest( + project_id, + compileAsUser, + options, + function( + error, + status, + outputFiles, + clsiServerId, + validationProblems + ) { + if (error != null) { + return callback(error) + } + logger.log({ files: outputFiles }, 'output files') + return callback( + null, + status, + outputFiles, + clsiServerId, + limits, + validationProblems + ) + } + ) + } + ) + } + ) + } + ) + } + ) + } + ) + }, + + stopCompile(project_id, user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return CompileManager.getProjectCompileLimits(project_id, function( + error, + limits + ) { + if (error != null) { + return callback(error) + } + return ClsiManager.stopCompile(project_id, user_id, limits, callback) + }) + }, + + deleteAuxFiles(project_id, user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return CompileManager.getProjectCompileLimits(project_id, function( + error, + limits + ) { + if (error != null) { + return callback(error) + } + return ClsiManager.deleteAuxFiles(project_id, user_id, limits, callback) + }) + }, + + getProjectCompileLimits(project_id, callback) { + if (callback == null) { + callback = function(error, limits) {} + } + return ProjectGetter.getProject(project_id, { owner_ref: 1 }, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + return UserGetter.getUser(project.owner_ref, { features: 1 }, function( + err, + owner + ) { + if (error != null) { + return callback(error) + } + return callback(null, { + timeout: + __guard__( + owner != null ? owner.features : undefined, + x => x.compileTimeout + ) || Settings.defaultFeatures.compileTimeout, + compileGroup: + __guard__( + owner != null ? owner.features : undefined, + x1 => x1.compileGroup + ) || Settings.defaultFeatures.compileGroup + }) + }) + }) + }, + + COMPILE_DELAY: 1, // seconds + _checkIfRecentlyCompiled(project_id, user_id, callback) { + if (callback == null) { + callback = function(error, recentlyCompiled) {} + } + const key = `compile:${project_id}:${user_id}` + return rclient.set(key, true, 'EX', this.COMPILE_DELAY, 'NX', function( + error, + ok + ) { + if (error != null) { + return callback(error) + } + if (ok === 'OK') { + return callback(null, false) + } else { + return callback(null, true) + } + }) + }, + + _checkCompileGroupAutoCompileLimit(isAutoCompile, compileGroup, callback) { + if (callback == null) { + callback = function(err, canCompile) {} + } + if (!isAutoCompile) { + return callback(null, true) + } + if (compileGroup === 'standard') { + // apply extra limits to the standard compile group + return CompileManager._checkIfAutoCompileLimitHasBeenHit( + isAutoCompile, + compileGroup, + callback + ) + } else { + Metrics.inc(`auto-compile-${compileGroup}`) + return callback(null, true) + } + }, // always allow priority group users to compile + + _checkIfAutoCompileLimitHasBeenHit(isAutoCompile, compileGroup, callback) { + if (callback == null) { + callback = function(err, canCompile) {} + } + if (!isAutoCompile) { + return callback(null, true) + } + Metrics.inc(`auto-compile-${compileGroup}`) + const opts = { + endpointName: 'auto_compile', + timeInterval: 20, + subjectName: compileGroup, + throttle: + __guard__( + __guard__( + Settings != null ? Settings.rateLimit : undefined, + x1 => x1.autoCompile + ), + x => x[compileGroup] + ) || 25 + } + return rateLimiter.addCount(opts, function(err, canCompile) { + if (err != null) { + canCompile = false + } + if (!canCompile) { + Metrics.inc(`auto-compile-${compileGroup}-limited`) + } + return callback(err, canCompile) + }) + }, + + wordCount(project_id, user_id, file, callback) { + if (callback == null) { + callback = function(error) {} + } + return CompileManager.getProjectCompileLimits(project_id, function( + error, + limits + ) { + if (error != null) { + return callback(error) + } + return ClsiManager.wordCount(project_id, user_id, file, limits, callback) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Contacts/ContactController.js b/services/web/app/src/Features/Contacts/ContactController.js new file mode 100644 index 0000000000..81e17b5ea4 --- /dev/null +++ b/services/web/app/src/Features/Contacts/ContactController.js @@ -0,0 +1,88 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ContactsController +const AuthenticationController = require('../Authentication/AuthenticationController') +const ContactManager = require('./ContactManager') +const UserGetter = require('../User/UserGetter') +const logger = require('logger-sharelatex') +const Modules = require('../../infrastructure/Modules') + +module.exports = ContactsController = { + getContacts(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return ContactManager.getContactIds(user_id, { limit: 50 }, function( + error, + contact_ids + ) { + if (error != null) { + return next(error) + } + return UserGetter.getUsers( + contact_ids, + { + email: 1, + first_name: 1, + last_name: 1, + holdingAccount: 1 + }, + function(error, contacts) { + if (error != null) { + return next(error) + } + + // UserGetter.getUsers may not preserve order so put them back in order + const positions = {} + for (let i = 0; i < contact_ids.length; i++) { + const contact_id = contact_ids[i] + positions[contact_id] = i + } + contacts.sort( + (a, b) => + positions[a._id != null ? a._id.toString() : undefined] - + positions[b._id != null ? b._id.toString() : undefined] + ) + + // Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc) + contacts = contacts.filter(c => !c.holdingAccount) + + contacts = contacts.map(ContactsController._formatContact) + + return Modules.hooks.fire('getContacts', user_id, contacts, function( + error, + additional_contacts + ) { + if (error != null) { + return next(error) + } + contacts = contacts.concat(...Array.from(additional_contacts || [])) + return res.send({ + contacts + }) + }) + } + ) + }) + }, + + _formatContact(contact) { + return { + id: contact._id != null ? contact._id.toString() : undefined, + email: contact.email || '', + first_name: contact.first_name || '', + last_name: contact.last_name || '', + type: 'user' + } + } +} diff --git a/services/web/app/src/Features/Contacts/ContactManager.js b/services/web/app/src/Features/Contacts/ContactManager.js new file mode 100644 index 0000000000..b97ba05d35 --- /dev/null +++ b/services/web/app/src/Features/Contacts/ContactManager.js @@ -0,0 +1,96 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ContactManager +const request = require('request') +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') + +module.exports = ContactManager = { + getContactIds(user_id, options, callback) { + if (options == null) { + options = { limits: 50 } + } + if (callback == null) { + callback = function(error, contacts) {} + } + logger.log({ user_id }, 'getting user contacts') + const url = `${settings.apis.contacts.url}/user/${user_id}/contacts` + return request.get( + { + url, + qs: options, + json: true, + jar: false + }, + function(error, res, data) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback( + null, + (data != null ? data.contact_ids : undefined) || [] + ) + } else { + error = new Error( + `contacts api responded with non-success code: ${res.statusCode}` + ) + logger.error( + { err: error, user_id }, + 'error getting contacts for user' + ) + return callback(error) + } + } + ) + }, + + addContact(user_id, contact_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ user_id, contact_id }, 'add user contact') + const url = `${settings.apis.contacts.url}/user/${user_id}/contacts` + return request.post( + { + url, + json: { + contact_id + }, + jar: false + }, + function(error, res, data) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback( + null, + (data != null ? data.contact_ids : undefined) || [] + ) + } else { + error = new Error( + `contacts api responded with non-success code: ${res.statusCode}` + ) + logger.error( + { err: error, user_id, contact_id }, + 'error adding contact for user' + ) + return callback(error) + } + } + ) + } +} diff --git a/services/web/app/src/Features/Contacts/ContactRouter.js b/services/web/app/src/Features/Contacts/ContactRouter.js new file mode 100644 index 0000000000..6e9a8ff78f --- /dev/null +++ b/services/web/app/src/Features/Contacts/ContactRouter.js @@ -0,0 +1,19 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthenticationController = require('../Authentication/AuthenticationController') +const ContactController = require('./ContactController') + +module.exports = { + apply(webRouter, apiRouter) { + return webRouter.get( + '/user/contacts', + AuthenticationController.requireLogin(), + ContactController.getContacts + ) + } +} diff --git a/services/web/app/src/Features/Cooldown/CooldownManager.js b/services/web/app/src/Features/Cooldown/CooldownManager.js new file mode 100644 index 0000000000..1a2a39d4a5 --- /dev/null +++ b/services/web/app/src/Features/Cooldown/CooldownManager.js @@ -0,0 +1,56 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CooldownManager +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('cooldown') +const logger = require('logger-sharelatex') + +const COOLDOWN_IN_SECONDS = 60 * 10 + +module.exports = CooldownManager = { + _buildKey(projectId) { + return `Cooldown:{${projectId}}` + }, + + putProjectOnCooldown(projectId, callback) { + if (callback == null) { + callback = function(err) {} + } + logger.log( + { projectId }, + `[Cooldown] putting project on cooldown for ${COOLDOWN_IN_SECONDS} seconds` + ) + return rclient.set( + CooldownManager._buildKey(projectId), + '1', + 'EX', + COOLDOWN_IN_SECONDS, + callback + ) + }, + + isProjectOnCooldown(projectId, callback) { + if (callback == null) { + callback = function(err, isOnCooldown) {} + } + return rclient.get(CooldownManager._buildKey(projectId), function( + err, + result + ) { + if (err != null) { + return callback(err) + } + return callback(null, result === '1') + }) + } +} diff --git a/services/web/app/src/Features/Cooldown/CooldownMiddleware.js b/services/web/app/src/Features/Cooldown/CooldownMiddleware.js new file mode 100644 index 0000000000..9baf008007 --- /dev/null +++ b/services/web/app/src/Features/Cooldown/CooldownMiddleware.js @@ -0,0 +1,40 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CooldownMiddleware +const CooldownManager = require('./CooldownManager') +const logger = require('logger-sharelatex') + +module.exports = CooldownMiddleware = { + freezeProject(req, res, next) { + const projectId = req.params.Project_id + if (projectId == null) { + return next(new Error('[Cooldown] No projectId parameter on route')) + } + return CooldownManager.isProjectOnCooldown(projectId, function( + err, + projectIsOnCooldown + ) { + if (err != null) { + return next(err) + } + if (projectIsOnCooldown) { + logger.log( + { projectId }, + '[Cooldown] project is on cooldown, denying request' + ) + return res.sendStatus(429) + } + return next() + }) + } +} diff --git a/services/web/app/src/Features/Docstore/DocstoreManager.js b/services/web/app/src/Features/Docstore/DocstoreManager.js new file mode 100644 index 0000000000..e43bcd1071 --- /dev/null +++ b/services/web/app/src/Features/Docstore/DocstoreManager.js @@ -0,0 +1,261 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DocstoreManager +const request = require('request').defaults({ jar: false }) +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const Errors = require('../Errors/Errors') + +module.exports = DocstoreManager = { + deleteDoc(project_id, doc_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id, doc_id }, 'deleting doc in docstore api') + const url = `${ + settings.apis.docstore.url + }/project/${project_id}/doc/${doc_id}` + return request.del(url, function(error, res, body) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null) + } else if (res.statusCode === 404) { + error = new Errors.NotFoundError('tried to delete doc not in docstore') + logger.error( + { err: error, project_id, doc_id }, + 'tried to delete doc not in docstore' + ) + return callback(error) // maybe suppress the error when delete doc which is not present? + } else { + error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.error( + { err: error, project_id, doc_id }, + 'error deleting doc in docstore' + ) + return callback(error) + } + }) + }, + + getAllDocs(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id }, 'getting all docs for project in docstore api') + const url = `${settings.apis.docstore.url}/project/${project_id}/doc` + return request.get( + { + url, + json: true + }, + function(error, res, docs) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, docs) + } else { + error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.error( + { err: error, project_id }, + 'error getting all docs from docstore' + ) + return callback(error) + } + } + ) + }, + + getAllRanges(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { project_id }, + 'getting all doc ranges for project in docstore api' + ) + const url = `${settings.apis.docstore.url}/project/${project_id}/ranges` + return request.get( + { + url, + json: true + }, + function(error, res, docs) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, docs) + } else { + error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.error( + { err: error, project_id }, + 'error getting all doc ranges from docstore' + ) + return callback(error) + } + } + ) + }, + + getDoc(project_id, doc_id, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function(error, lines, rev, version) {} + } + if (typeof options === 'function') { + callback = options + options = {} + } + logger.log({ project_id, doc_id, options }, 'getting doc in docstore api') + let url = `${ + settings.apis.docstore.url + }/project/${project_id}/doc/${doc_id}` + if (options.include_deleted) { + url += '?include_deleted=true' + } + return request.get( + { + url, + json: true + }, + function(error, res, doc) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log( + { doc_id, project_id, version: doc.version, rev: doc.rev }, + 'got doc from docstore api' + ) + return callback(null, doc.lines, doc.rev, doc.version, doc.ranges) + } else if (res.statusCode === 404) { + error = new Errors.NotFoundError('doc not found in docstore') + logger.error( + { err: error, project_id, doc_id }, + 'doc not found in docstore' + ) + return callback(error) + } else { + error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.error( + { err: error, project_id, doc_id }, + 'error getting doc from docstore' + ) + return callback(error) + } + } + ) + }, + + updateDoc(project_id, doc_id, lines, version, ranges, callback) { + if (callback == null) { + callback = function(error, modified, rev) {} + } + logger.log({ project_id, doc_id }, 'updating doc in docstore api') + const url = `${ + settings.apis.docstore.url + }/project/${project_id}/doc/${doc_id}` + return request.post( + { + url, + json: { + lines, + version, + ranges + } + }, + function(error, res, result) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log( + { project_id, doc_id }, + 'update doc in docstore url finished' + ) + return callback(null, result.modified, result.rev) + } else { + error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.error( + { err: error, project_id, doc_id }, + 'error updating doc in docstore' + ) + return callback(error) + } + } + ) + }, + + archiveProject(project_id, callback) { + const url = `${settings.apis.docstore.url}/project/${project_id}/archive` + logger.log({ project_id }, 'archiving project in docstore') + return request.post(url, function(err, res, docs) { + if (err != null) { + logger.err({ err, project_id }, 'error archving project in docstore') + return callback(err) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback() + } else { + const error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.err( + { err: error, project_id }, + 'error archiving project in docstore' + ) + return callback(error) + } + }) + }, + + unarchiveProject(project_id, callback) { + const url = `${settings.apis.docstore.url}/project/${project_id}/unarchive` + logger.log({ project_id }, 'unarchiving project in docstore') + return request.post(url, function(err, res, docs) { + if (err != null) { + logger.err({ err, project_id }, 'error unarchiving project in docstore') + return callback(err) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback() + } else { + const error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.err( + { err: error, project_id }, + 'error unarchiving project in docstore' + ) + return callback(error) + } + }) + } +} diff --git a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js new file mode 100644 index 0000000000..18d0720b0a --- /dev/null +++ b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js @@ -0,0 +1,467 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DocumentUpdaterHandler +let request = require('request') +request = request.defaults() +const settings = require('settings-sharelatex') +const _ = require('underscore') +const async = require('async') +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const { Project } = require('../../models/Project') + +module.exports = DocumentUpdaterHandler = { + flushProjectToMongo(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id }, 'flushing project from document updater') + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/flush`, + method: 'POST' + }, + project_id, + 'flushing.mongo.project', + callback + ) + }, + + flushMultipleProjectsToMongo(project_ids, callback) { + if (callback == null) { + callback = function(error) {} + } + const jobs = [] + for (let project_id of Array.from(project_ids)) { + ;(project_id => + jobs.push(callback => + DocumentUpdaterHandler.flushProjectToMongo(project_id, callback) + ))(project_id) + } + return async.series(jobs, callback) + }, + + flushProjectToMongoAndDelete(project_id, callback) { + if (callback == null) { + callback = function() {} + } + const timer = new metrics.Timer('delete.mongo.project') + const url = `${settings.apis.documentupdater.url}` + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}`, + method: 'DELETE' + }, + project_id, + 'flushing.mongo.project', + callback + ) + }, + + flushDocToMongo(project_id, doc_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id, doc_id }, 'flushing doc from document updater') + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/doc/${doc_id}/flush`, + method: 'POST' + }, + project_id, + 'flushing.mongo.doc', + callback + ) + }, + + deleteDoc(project_id, doc_id, callback) { + if (callback == null) { + callback = function() {} + } + logger.log({ project_id, doc_id }, 'deleting doc from document updater') + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/doc/${doc_id}`, + method: 'DELETE' + }, + project_id, + 'delete.mongo.doc', + callback + ) + }, + + getDocument(project_id, doc_id, fromVersion, callback) { + if (callback == null) { + callback = function(error, doclines, version, ranges, ops) {} + } + logger.log({ project_id, doc_id }, 'getting doc from document updater') + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/doc/${doc_id}?fromVersion=${fromVersion}`, + json: true + }, + project_id, + 'get-document', + function(error, doc) { + if (error != null) { + return callback(error) + } + return callback(null, doc.lines, doc.version, doc.ranges, doc.ops) + } + ) + }, + + setDocument(project_id, doc_id, user_id, docLines, source, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { project_id, doc_id, source, user_id }, + 'setting doc in document updater' + ) + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/doc/${doc_id}`, + method: 'POST', + json: { + lines: docLines, + source, + user_id + } + }, + project_id, + 'set-document', + callback + ) + }, + + getProjectDocsIfMatch(project_id, projectStateHash, callback) { + // If the project state hasn't changed, we can get all the latest + // docs from redis via the docupdater. Otherwise we will need to + // fall back to getting them from mongo. + if (callback == null) { + callback = function(error, docs) {} + } + const timer = new metrics.Timer('get-project-docs') + const url = `${ + settings.apis.documentupdater.url + }/project/${project_id}/get_and_flush_if_old?state=${projectStateHash}` + logger.log({ project_id }, 'getting project docs from document updater') + return request.post(url, function(error, res, body) { + timer.done() + if (error != null) { + logger.error( + { err: error, url, project_id }, + 'error getting project docs from doc updater' + ) + return callback(error) + } + if (res.statusCode === 409) { + // HTTP response code "409 Conflict" + // Docupdater has checked the projectStateHash and found that + // it has changed. This means that the docs currently in redis + // aren't the only change to the project and the full set of + // docs/files should be retreived from docstore/filestore + // instead. + return callback() + } else if (res.statusCode >= 200 && res.statusCode < 300) { + let docs + logger.log( + { project_id }, + 'got project docs from document document updater' + ) + try { + docs = JSON.parse(body) + } catch (error1) { + error = error1 + return callback(error) + } + return callback(null, docs) + } else { + logger.error( + { project_id, url }, + `doc updater returned a non-success status code: ${res.statusCode}` + ) + return callback( + new Error( + `doc updater returned a non-success status code: ${res.statusCode}` + ) + ) + } + }) + }, + + clearProjectState(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id }, 'clearing project state from document updater') + + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/clearState`, + method: 'POST' + }, + project_id, + 'clear-project-state', + callback + ) + }, + + acceptChanges(project_id, doc_id, change_ids, callback) { + if (change_ids == null) { + change_ids = [] + } + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id, doc_id }, `accepting ${change_ids.length} changes`) + + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/doc/${doc_id}/change/accept`, + json: { + change_ids + }, + method: 'POST' + }, + project_id, + 'accept-changes', + callback + ) + }, + + deleteThread(project_id, doc_id, thread_id, callback) { + if (callback == null) { + callback = function(error) {} + } + const timer = new metrics.Timer('delete-thread') + logger.log( + { project_id, doc_id, thread_id }, + 'deleting comment range in document updater' + ) + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/doc/${doc_id}/comment/${thread_id}`, + method: 'DELETE' + }, + project_id, + 'delete-thread', + callback + ) + }, + + resyncProjectHistory(project_id, projectHistoryId, docs, files, callback) { + logger.info( + { project_id, docs, files }, + 'resyncing project history in doc updater' + ) + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}/history/resync`, + json: { docs, files, projectHistoryId }, + method: 'POST' + }, + project_id, + 'resync-project-history', + callback + ) + }, + + updateProjectStructure( + project_id, + projectHistoryId, + userId, + changes, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + if ( + !(settings.apis.project_history != null + ? settings.apis.project_history.sendProjectStructureOps + : undefined) + ) { + return callback() + } + + const docUpdates = DocumentUpdaterHandler._getUpdates( + 'doc', + changes.oldDocs, + changes.newDocs + ) + const fileUpdates = DocumentUpdaterHandler._getUpdates( + 'file', + changes.oldFiles, + changes.newFiles + ) + const projectVersion = __guard__( + changes != null ? changes.newProject : undefined, + x => x.version + ) + + if (docUpdates.length + fileUpdates.length < 1) { + return callback() + } + + if (projectVersion == null) { + logger.error( + { project_id, changes, projectVersion }, + 'did not receive project version in changes' + ) + return callback(new Error('did not receive project version in changes')) + } + + logger.log({ project_id }, 'updating project structure in doc updater') + return DocumentUpdaterHandler._makeRequest( + { + path: `/project/${project_id}`, + json: { + docUpdates, + fileUpdates, + userId, + version: projectVersion, + projectHistoryId + }, + method: 'POST' + }, + project_id, + 'update-project-structure', + callback + ) + }, + + _makeRequest(options, project_id, metricsKey, callback) { + const timer = new metrics.Timer(metricsKey) + return request( + { + url: `${settings.apis.documentupdater.url}${options.path}`, + json: options.json, + method: options.method || 'GET' + }, + function(error, res, body) { + timer.done() + if (error != null) { + logger.error( + { error, project_id }, + 'error making request to document updater' + ) + return callback(error) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body) + } else { + error = new Error( + `document updater returned a failure status code: ${res.statusCode}` + ) + logger.error( + { error, project_id }, + `document updater returned failure status code: ${res.statusCode}` + ) + return callback(error) + } + } + ) + }, + + _getUpdates(entityType, oldEntities, newEntities) { + let id, newEntity, oldEntity + if (!oldEntities) { + oldEntities = [] + } + if (!newEntities) { + newEntities = [] + } + const updates = [] + + const oldEntitiesHash = _.indexBy(oldEntities, entity => + entity[entityType]._id.toString() + ) + const newEntitiesHash = _.indexBy(newEntities, entity => + entity[entityType]._id.toString() + ) + + // Send deletes before adds (and renames) to keep a 1:1 mapping between + // paths and ids + // + // When a file is replaced, we first delete the old file and then add the + // new file. If the 'add' operation is sent to project history before the + // 'delete' then we would have two files with the same path at that point + // in time. + for (id in oldEntitiesHash) { + oldEntity = oldEntitiesHash[id] + newEntity = newEntitiesHash[id] + + if (newEntity == null) { + // entity deleted + updates.push({ + id, + pathname: oldEntity.path, + newPathname: '' + }) + } + } + + for (id in newEntitiesHash) { + newEntity = newEntitiesHash[id] + oldEntity = oldEntitiesHash[id] + + if (oldEntity == null) { + // entity added + updates.push({ + id, + pathname: newEntity.path, + docLines: newEntity.docLines, + url: newEntity.url, + hash: newEntity.file != null ? newEntity.file.hash : undefined + }) + } else if (newEntity.path !== oldEntity.path) { + // entity renamed + updates.push({ + id, + pathname: oldEntity.path, + newPathname: newEntity.path + }) + } + } + + return updates + } +} + +const PENDINGUPDATESKEY = 'PendingUpdates' +const DOCLINESKEY = 'doclines' +const DOCIDSWITHPENDINGUPDATES = 'DocsWithPendingUpdates' + +const keys = { + pendingUpdates(op) { + return `${PENDINGUPDATESKEY}:${op.doc_id}` + }, + docsWithPendingUpdates: DOCIDSWITHPENDINGUPDATES, + docLines(op) { + return `${DOCLINESKEY}:${op.doc_id}` + }, + combineProjectIdAndDocId(project_id, doc_id) { + return `${project_id}:${doc_id}` + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Documents/DocumentController.js b/services/web/app/src/Features/Documents/DocumentController.js new file mode 100644 index 0000000000..a9b8e05ba3 --- /dev/null +++ b/services/web/app/src/Features/Documents/DocumentController.js @@ -0,0 +1,135 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectLocator = require('../Project/ProjectLocator') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const logger = require('logger-sharelatex') + +module.exports = { + getDocument(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + const { doc_id } = req.params + const plain = + __guard__(req != null ? req.query : undefined, x => x.plain) === 'true' + logger.log( + { doc_id, project_id }, + 'receiving get document request from api (docupdater)' + ) + return ProjectGetter.getProject( + project_id, + { rootFolder: true, overleaf: true }, + function(error, project) { + if (error != null) { + return next(error) + } + if (project == null) { + return res.sendStatus(404) + } + return ProjectLocator.findElement( + { project, element_id: doc_id, type: 'doc' }, + function(error, doc, path) { + if (error != null) { + logger.err( + { err: error, doc_id, project_id }, + 'error finding element for getDocument' + ) + return next(error) + } + return ProjectEntityHandler.getDoc(project_id, doc_id, function( + error, + lines, + rev, + version, + ranges + ) { + if (error != null) { + logger.err( + { err: error, doc_id, project_id }, + 'error finding doc contents for getDocument' + ) + return next(error) + } + if (plain) { + res.type('text/plain') + return res.send(lines.join('\n')) + } else { + const projectHistoryId = __guard__( + __guard__( + project != null ? project.overleaf : undefined, + x2 => x2.history + ), + x1 => x1.id + ) + return res.json({ + lines, + version, + ranges, + pathname: path.fileSystem, + projectHistoryId + }) + } + }) + } + ) + } + ) + }, + + setDocument(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + const { doc_id } = req.params + const { lines, version, ranges, lastUpdatedAt, lastUpdatedBy } = req.body + logger.log( + { doc_id, project_id }, + 'receiving set document request from api (docupdater)' + ) + return ProjectEntityUpdateHandler.updateDocLines( + project_id, + doc_id, + lines, + version, + ranges, + lastUpdatedAt, + lastUpdatedBy, + function(error) { + if (error != null) { + logger.err( + { err: error, doc_id, project_id }, + 'error finding element for getDocument' + ) + return next(error) + } + logger.log( + { doc_id, project_id }, + 'finished receiving set document request from api (docupdater)' + ) + return res.sendStatus(200) + } + ) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Documents/DocumentHelper.js b/services/web/app/src/Features/Documents/DocumentHelper.js new file mode 100644 index 0000000000..fd0d67a6f7 --- /dev/null +++ b/services/web/app/src/Features/Documents/DocumentHelper.js @@ -0,0 +1,78 @@ +/* eslint-disable + max-len, + no-cond-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DocumentHelper +module.exports = DocumentHelper = { + getTitleFromTexContent(content, maxContentToScan) { + if (maxContentToScan == null) { + maxContentToScan = 30000 + } + const TITLE_WITH_CURLY_BRACES = /\\[tT]itle\*?\s*{([^}]+)}/ + const TITLE_WITH_SQUARE_BRACES = /\\[tT]itle\s*\[([^\]]+)\]/ + for (let line of Array.from( + DocumentHelper._getLinesFromContent(content, maxContentToScan) + )) { + var match + if ( + (match = + line.match(TITLE_WITH_CURLY_BRACES) || + line.match(TITLE_WITH_SQUARE_BRACES)) + ) { + return DocumentHelper.detex(match[1]) + } + } + + return null + }, + + contentHasDocumentclass(content, maxContentToScan) { + if (maxContentToScan == null) { + maxContentToScan = 30000 + } + for (let line of Array.from( + DocumentHelper._getLinesFromContent(content, maxContentToScan) + )) { + // We've had problems with this regex locking up CPU. + // Previously /.*\\documentclass/ would totally lock up on lines of 500kb (data text files :() + // This regex will only look from the start of the line, including whitespace so will return quickly + // regardless of line length. + if (line.match(/^\s*\\documentclass/)) { + return true + } + } + + return false + }, + + detex(string) { + return string + .replace(/\\LaTeX/g, 'LaTeX') + .replace(/\\TeX/g, 'TeX') + .replace(/\\TikZ/g, 'TikZ') + .replace(/\\BibTeX/g, 'BibTeX') + .replace(/\\\[[A-Za-z0-9. ]*\]/g, ' ') // line spacing + .replace(/\\(?:[a-zA-Z]+|.|)/g, '') + .replace(/{}|~/g, ' ') + .replace(/[${}]/g, '') + .replace(/ +/g, ' ') + .trim() + }, + + _getLinesFromContent(content, maxContentToScan) { + if (typeof content === 'string') { + return content.substring(0, maxContentToScan).split('\n') + } else { + return content + } + } +} diff --git a/services/web/app/src/Features/Downloads/ProjectDownloadsController.js b/services/web/app/src/Features/Downloads/ProjectDownloadsController.js new file mode 100644 index 0000000000..8c8158a8b8 --- /dev/null +++ b/services/web/app/src/Features/Downloads/ProjectDownloadsController.js @@ -0,0 +1,82 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectDownloadsController +const logger = require('logger-sharelatex') +const Metrics = require('metrics-sharelatex') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectZipStreamManager = require('./ProjectZipStreamManager') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') + +module.exports = ProjectDownloadsController = { + downloadProject(req, res, next) { + const project_id = req.params.Project_id + Metrics.inc('zip-downloads') + logger.log({ project_id }, 'downloading project') + return DocumentUpdaterHandler.flushProjectToMongo(project_id, function( + error + ) { + if (error != null) { + return next(error) + } + return ProjectGetter.getProject(project_id, { name: true }, function( + error, + project + ) { + if (error != null) { + return next(error) + } + return ProjectZipStreamManager.createZipStreamForProject( + project_id, + function(error, stream) { + if (error != null) { + return next(error) + } + res.setContentDisposition('attachment', { + filename: `${project.name}.zip` + }) + res.contentType('application/zip') + return stream.pipe(res) + } + ) + }) + }) + }, + + downloadMultipleProjects(req, res, next) { + const project_ids = req.query.project_ids.split(',') + Metrics.inc('zip-downloads-multiple') + logger.log({ project_ids }, 'downloading multiple projects') + return DocumentUpdaterHandler.flushMultipleProjectsToMongo( + project_ids, + function(error) { + if (error != null) { + return next(error) + } + return ProjectZipStreamManager.createZipStreamForMultipleProjects( + project_ids, + function(error, stream) { + if (error != null) { + return next(error) + } + res.setContentDisposition('attachment', { + filename: `Overleaf Projects (${project_ids.length} items).zip` + }) + res.contentType('application/zip') + return stream.pipe(res) + } + ) + } + ) + } +} diff --git a/services/web/app/src/Features/Downloads/ProjectZipStreamManager.js b/services/web/app/src/Features/Downloads/ProjectZipStreamManager.js new file mode 100644 index 0000000000..ece438fdf7 --- /dev/null +++ b/services/web/app/src/Features/Downloads/ProjectZipStreamManager.js @@ -0,0 +1,179 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectZipStreamManager +const archiver = require('archiver') +const async = require('async') +const logger = require('logger-sharelatex') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const ProjectGetter = require('../Project/ProjectGetter') +const FileStoreHandler = require('../FileStore/FileStoreHandler') + +module.exports = ProjectZipStreamManager = { + createZipStreamForMultipleProjects(project_ids, callback) { + // We'll build up a zip file that contains multiple zip files + + if (callback == null) { + callback = function(error, stream) {} + } + const archive = archiver('zip') + archive.on('error', err => + logger.err( + { err, project_ids }, + 'something went wrong building archive of project' + ) + ) + callback(null, archive) + + logger.log({ project_ids }, 'creating zip stream of multiple projects') + + const jobs = [] + for (let project_id of Array.from(project_ids || [])) { + ;(project_id => + jobs.push(callback => + ProjectGetter.getProject(project_id, { name: true }, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + logger.log( + { project_id, name: project.name }, + 'appending project to zip stream' + ) + return ProjectZipStreamManager.createZipStreamForProject( + project_id, + function(error, stream) { + if (error != null) { + return callback(error) + } + archive.append(stream, { name: `${project.name}.zip` }) + return stream.on('end', function() { + logger.log( + { project_id, name: project.name }, + 'zip stream ended' + ) + return callback() + }) + } + ) + }) + ))(project_id) + } + + return async.series(jobs, function() { + logger.log( + { project_ids }, + 'finished creating zip stream of multiple projects' + ) + return archive.finalize() + }) + }, + + createZipStreamForProject(project_id, callback) { + if (callback == null) { + callback = function(error, stream) {} + } + const archive = archiver('zip') + // return stream immediately before we start adding things to it + archive.on('error', err => + logger.err( + { err, project_id }, + 'something went wrong building archive of project' + ) + ) + callback(null, archive) + return this.addAllDocsToArchive(project_id, archive, error => { + if (error != null) { + logger.error( + { err: error, project_id }, + 'error adding docs to zip stream' + ) + } + return this.addAllFilesToArchive(project_id, archive, error => { + if (error != null) { + logger.error( + { err: error, project_id }, + 'error adding files to zip stream' + ) + } + return archive.finalize() + }) + }) + }, + + addAllDocsToArchive(project_id, archive, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectEntityHandler.getAllDocs(project_id, function(error, docs) { + if (error != null) { + return callback(error) + } + const jobs = [] + for (let path in docs) { + const doc = docs[path] + ;(function(path, doc) { + if (path[0] === '/') { + path = path.slice(1) + } + return jobs.push(function(callback) { + logger.log({ project_id }, 'Adding doc') + archive.append(doc.lines.join('\n'), { name: path }) + return callback() + }) + })(path, doc) + } + return async.series(jobs, callback) + }) + }, + + addAllFilesToArchive(project_id, archive, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectEntityHandler.getAllFiles(project_id, function(error, files) { + if (error != null) { + return callback(error) + } + const jobs = [] + for (let path in files) { + const file = files[path] + ;((path, file) => + jobs.push(callback => + FileStoreHandler.getFileStream(project_id, file._id, {}, function( + error, + stream + ) { + if (error != null) { + logger.err( + { err: error, project_id, file_id: file._id }, + 'something went wrong adding file to zip archive' + ) + return callback(err) + } + if (path[0] === '/') { + path = path.slice(1) + } + archive.append(stream, { name: path }) + return stream.on('end', () => callback()) + }) + ))(path, file) + } + return async.parallelLimit(jobs, 5, callback) + }) + } +} diff --git a/services/web/app/src/Features/Editor/EditorController.js b/services/web/app/src/Features/Editor/EditorController.js new file mode 100644 index 0000000000..4a62c4c1e8 --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorController.js @@ -0,0 +1,727 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-dupe-keys, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let EditorController +const logger = require('logger-sharelatex') +const Metrics = require('metrics-sharelatex') +const sanitize = require('sanitizer') +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const ProjectDeleter = require('../Project/ProjectDeleter') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const EditorRealTimeController = require('./EditorRealTimeController') +const async = require('async') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const _ = require('underscore') + +module.exports = EditorController = { + addDoc(project_id, folder_id, docName, docLines, source, user_id, callback) { + if (callback == null) { + callback = function(error, doc) {} + } + return EditorController.addDocWithRanges( + project_id, + folder_id, + docName, + docLines, + {}, + source, + user_id, + callback + ) + }, + + addDocWithRanges( + project_id, + folder_id, + docName, + docLines, + docRanges, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function(error, doc) {} + } + docName = docName.trim() + logger.log( + { project_id, folder_id, docName, source }, + 'sending new doc to project' + ) + Metrics.inc('editor.add-doc') + return ProjectEntityUpdateHandler.addDocWithRanges( + project_id, + folder_id, + docName, + docLines, + docRanges, + user_id, + (err, doc, folder_id) => { + if (err != null) { + logger.err( + { err, project_id, docName }, + 'error adding doc without lock' + ) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewDoc', + folder_id, + doc, + source + ) + return callback(err, doc) + } + ) + }, + + addFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function(error, file) {} + } + fileName = fileName.trim() + logger.log( + { + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + source, + user_id + }, + 'sending new file to project' + ) + Metrics.inc('editor.add-file') + return ProjectEntityUpdateHandler.addFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + user_id, + (err, fileRef, folder_id) => { + if (err != null) { + logger.err( + { err, project_id, folder_id, fileName }, + 'error adding file without lock' + ) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFile', + folder_id, + fileRef, + source, + linkedFileData + ) + return callback(err, fileRef) + } + ) + }, + + upsertDoc( + project_id, + folder_id, + docName, + docLines, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function(err) {} + } + return ProjectEntityUpdateHandler.upsertDoc( + project_id, + folder_id, + docName, + docLines, + source, + user_id, + function(err, doc, didAddNewDoc) { + if (didAddNewDoc) { + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewDoc', + folder_id, + doc, + source + ) + } + return callback(err, doc) + } + ) + }, + + upsertFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function(err, file) {} + } + return ProjectEntityUpdateHandler.upsertFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + user_id, + function(err, newFile, didAddFile, existingFile) { + if (err != null) { + return callback(err) + } + if (!didAddFile) { + // replacement, so remove the existing file from the client + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + existingFile._id, + source + ) + } + // now add the new file on the client + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFile', + folder_id, + newFile, + source, + linkedFileData + ) + return callback(null, newFile) + } + ) + }, + + upsertDocWithPath( + project_id, + elementPath, + docLines, + source, + user_id, + callback + ) { + return ProjectEntityUpdateHandler.upsertDocWithPath( + project_id, + elementPath, + docLines, + source, + user_id, + function(err, doc, didAddNewDoc, newFolders, lastFolder) { + if (err != null) { + return callback(err) + } + return EditorController._notifyProjectUsersOfNewFolders( + project_id, + newFolders, + function(err) { + if (err != null) { + return callback(err) + } + if (didAddNewDoc) { + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewDoc', + lastFolder._id, + doc, + source + ) + } + return callback() + } + ) + } + ) + }, + + upsertFileWithPath( + project_id, + elementPath, + fsPath, + linkedFileData, + source, + user_id, + callback + ) { + return ProjectEntityUpdateHandler.upsertFileWithPath( + project_id, + elementPath, + fsPath, + linkedFileData, + user_id, + function(err, newFile, didAddFile, existingFile, newFolders, lastFolder) { + if (err != null) { + return callback(err) + } + return EditorController._notifyProjectUsersOfNewFolders( + project_id, + newFolders, + function(err) { + if (err != null) { + return callback(err) + } + if (!didAddFile) { + // replacement, so remove the existing file from the client + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + existingFile._id, + source + ) + } + // now add the new file on the client + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFile', + lastFolder._id, + newFile, + source, + linkedFileData + ) + return callback() + } + ) + } + ) + }, + + addFolder(project_id, folder_id, folderName, source, callback) { + if (callback == null) { + callback = function(error, folder) {} + } + folderName = folderName.trim() + logger.log( + { project_id, folder_id, folderName, source }, + 'sending new folder to project' + ) + Metrics.inc('editor.add-folder') + return ProjectEntityUpdateHandler.addFolder( + project_id, + folder_id, + folderName, + (err, folder, folder_id) => { + if (err != null) { + logger.err( + { err, project_id, folder_id, folderName, source }, + 'could not add folder' + ) + return callback(err) + } + return EditorController._notifyProjectUsersOfNewFolder( + project_id, + folder_id, + folder, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, folder) + } + ) + } + ) + }, + + mkdirp(project_id, path, callback) { + if (callback == null) { + callback = function(error, newFolders, lastFolder) {} + } + logger.log({ project_id, path }, "making directories if they don't exist") + return ProjectEntityUpdateHandler.mkdirp( + project_id, + path, + (err, newFolders, lastFolder) => { + if (err != null) { + logger.err({ err, project_id, path }, 'could not mkdirp') + return callback(err) + } + + return EditorController._notifyProjectUsersOfNewFolders( + project_id, + newFolders, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, newFolders, lastFolder) + } + ) + } + ) + }, + + deleteEntity(project_id, entity_id, entityType, source, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { project_id, entity_id, entityType, source }, + 'start delete process of entity' + ) + Metrics.inc('editor.delete-entity') + return ProjectEntityUpdateHandler.deleteEntity( + project_id, + entity_id, + entityType, + userId, + function(err) { + if (err != null) { + logger.err( + { err, project_id, entity_id, entityType }, + 'could not delete entity' + ) + return callback(err) + } + logger.log( + { project_id, entity_id, entityType }, + 'telling users entity has been deleted' + ) + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + entity_id, + source + ) + return callback() + } + ) + }, + + deleteEntityWithPath(project_id, path, source, user_id, callback) { + return ProjectEntityUpdateHandler.deleteEntityWithPath( + project_id, + path, + user_id, + function(err, entity_id) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + entity_id, + source + ) + return callback(null, entity_id) + } + ) + }, + + notifyUsersProjectHasBeenDeletedOrRenamed(project_id, callback) { + EditorRealTimeController.emitToRoom( + project_id, + 'projectRenamedOrDeletedByExternalSource' + ) + return callback() + }, + + updateProjectDescription(project_id, description, callback) { + if (callback == null) { + callback = function() {} + } + logger.log({ project_id, description }, 'updating project description') + return ProjectDetailsHandler.setProjectDescription( + project_id, + description, + function(err) { + if (err != null) { + logger.err( + { err, project_id, description }, + 'something went wrong setting the project description' + ) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'projectDescriptionUpdated', + description + ) + return callback() + } + ) + }, + + deleteProject(project_id, callback) { + Metrics.inc('editor.delete-project') + logger.log({ project_id }, 'recived message to delete project') + return ProjectDeleter.deleteProject(project_id, callback) + }, + + renameEntity(project_id, entity_id, entityType, newName, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + newName = sanitize.escape(newName) + Metrics.inc('editor.rename-entity') + logger.log( + { entity_id, entity_id, entity_id }, + 'reciving new name for entity for project' + ) + return ProjectEntityUpdateHandler.renameEntity( + project_id, + entity_id, + entityType, + newName, + userId, + function(err) { + if (err != null) { + logger.err( + { err, project_id, entity_id, entityType, newName }, + 'error renaming entity' + ) + return callback(err) + } + if (newName.length > 0) { + EditorRealTimeController.emitToRoom( + project_id, + 'reciveEntityRename', + entity_id, + newName + ) + } + return callback() + } + ) + }, + + moveEntity(project_id, entity_id, folder_id, entityType, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + Metrics.inc('editor.move-entity') + return ProjectEntityUpdateHandler.moveEntity( + project_id, + entity_id, + folder_id, + entityType, + userId, + function(err) { + if (err != null) { + logger.err( + { err, project_id, entity_id, folder_id }, + 'error moving entity' + ) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'reciveEntityMove', + entity_id, + folder_id + ) + return callback() + } + ) + }, + + renameProject(project_id, newName, callback) { + if (callback == null) { + callback = function(err) {} + } + return ProjectDetailsHandler.renameProject(project_id, newName, function( + err + ) { + if (err != null) { + logger.err({ err, project_id, newName }, 'error renaming project') + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'projectNameUpdated', + newName + ) + return callback() + }) + }, + + setCompiler(project_id, compiler, callback) { + if (callback == null) { + callback = function(err) {} + } + return ProjectOptionsHandler.setCompiler(project_id, compiler, function( + err + ) { + if (err != null) { + return callback(err) + } + logger.log({ compiler, project_id }, 'setting compiler') + EditorRealTimeController.emitToRoom( + project_id, + 'compilerUpdated', + compiler + ) + return callback() + }) + }, + + setImageName(project_id, imageName, callback) { + if (callback == null) { + callback = function(err) {} + } + return ProjectOptionsHandler.setImageName(project_id, imageName, function( + err + ) { + if (err != null) { + return callback(err) + } + logger.log({ imageName, project_id }, 'setting imageName') + EditorRealTimeController.emitToRoom( + project_id, + 'imageNameUpdated', + imageName + ) + return callback() + }) + }, + + setSpellCheckLanguage(project_id, languageCode, callback) { + if (callback == null) { + callback = function(err) {} + } + return ProjectOptionsHandler.setSpellCheckLanguage( + project_id, + languageCode, + function(err) { + if (err != null) { + return callback(err) + } + logger.log( + { languageCode, project_id }, + 'setting languageCode for spell check' + ) + EditorRealTimeController.emitToRoom( + project_id, + 'spellCheckLanguageUpdated', + languageCode + ) + return callback() + } + ) + }, + + setPublicAccessLevel(project_id, newAccessLevel, callback) { + if (callback == null) { + callback = function(err) {} + } + return ProjectDetailsHandler.setPublicAccessLevel( + project_id, + newAccessLevel, + function(err) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'project:publicAccessLevel:changed', + { newAccessLevel } + ) + if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) { + return ProjectDetailsHandler.ensureTokensArePresent( + project_id, + function(err, tokens) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'project:tokens:changed', + { tokens } + ) + return callback() + } + ) + } else { + return callback() + } + } + ) + }, + + setRootDoc(project_id, newRootDocID, callback) { + if (callback == null) { + callback = function(err) {} + } + return ProjectEntityUpdateHandler.setRootDoc( + project_id, + newRootDocID, + function(err) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'rootDocUpdated', + newRootDocID + ) + return callback() + } + ) + }, + + _notifyProjectUsersOfNewFolders(project_id, folders, callback) { + if (callback == null) { + callback = function(error) {} + } + return async.eachSeries( + folders, + (folder, cb) => + EditorController._notifyProjectUsersOfNewFolder( + project_id, + folder.parentFolder_id, + folder, + cb + ), + callback + ) + }, + + _notifyProjectUsersOfNewFolder(project_id, folder_id, folder, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { project_id, folder, parentFolder_id: folder_id }, + 'sending newly created folder out to users' + ) + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFolder', + folder_id, + folder + ) + return callback() + } +} diff --git a/services/web/app/src/Features/Editor/EditorHttpController.js b/services/web/app/src/Features/Editor/EditorHttpController.js new file mode 100644 index 0000000000..75a27449a6 --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorHttpController.js @@ -0,0 +1,283 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let EditorHttpController +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const ProjectDeleter = require('../Project/ProjectDeleter') +const logger = require('logger-sharelatex') +const EditorRealTimeController = require('./EditorRealTimeController') +const EditorController = require('./EditorController') +const ProjectGetter = require('../Project/ProjectGetter') +const UserGetter = require('../User/UserGetter') +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const ProjectEditorHandler = require('../Project/ProjectEditorHandler') +const Metrics = require('metrics-sharelatex') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsInviteHandler = require('../Collaborators/CollaboratorsInviteHandler') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = EditorHttpController = { + joinProject(req, res, next) { + const project_id = req.params.Project_id + let { user_id } = req.query + if (user_id === 'anonymous-user') { + user_id = null + } + logger.log({ user_id, project_id }, 'join project request') + Metrics.inc('editor.join-project') + return EditorHttpController._buildJoinProjectView( + req, + project_id, + user_id, + function(error, project, privilegeLevel) { + if (error != null) { + return next(error) + } + // Hide access tokens if this is not the project owner + TokenAccessHandler.protectTokens(project, privilegeLevel) + res.json({ + project, + privilegeLevel + }) + // Only show the 'renamed or deleted' message once + if (project != null ? project.deletedByExternalDataSource : undefined) { + return ProjectDeleter.unmarkAsDeletedByExternalSource(project_id) + } + } + ) + }, + + _buildJoinProjectView(req, project_id, user_id, callback) { + if (callback == null) { + callback = function(error, project, privilegeLevel) {} + } + logger.log({ project_id, user_id }, 'building the joinProject view') + return ProjectGetter.getProjectWithoutDocLines(project_id, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + if (project == null) { + return callback(new Error('not found')) + } + return CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels( + project_id, + function(error, members) { + if (error != null) { + return callback(error) + } + const token = TokenAccessHandler.getRequestToken(req, project_id) + return AuthorizationManager.getPrivilegeLevelForProject( + user_id, + project_id, + token, + function(error, privilegeLevel) { + if (error != null) { + return callback(error) + } + if ( + privilegeLevel == null || + privilegeLevel === PrivilegeLevels.NONE + ) { + logger.log( + { project_id, user_id, privilegeLevel }, + 'not an acceptable privilege level, returning null' + ) + return callback(null, null, false) + } + return CollaboratorsInviteHandler.getAllInvites( + project_id, + function(error, invites) { + if (error != null) { + return callback(error) + } + logger.log( + { + project_id, + user_id, + memberCount: members.length, + inviteCount: invites.length, + privilegeLevel + }, + 'returning project model view' + ) + return callback( + null, + ProjectEditorHandler.buildProjectModelView( + project, + members, + invites + ), + privilegeLevel + ) + } + ) + } + ) + } + ) + }) + }, + + _nameIsAcceptableLength(name) { + return name != null && name.length < 150 && name.length !== 0 + }, + + addDoc(req, res, next) { + const project_id = req.params.Project_id + const { name } = req.body + const { parent_folder_id } = req.body + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log( + { project_id, name, parent_folder_id }, + 'getting request to add doc to project' + ) + if (!EditorHttpController._nameIsAcceptableLength(name)) { + return res.sendStatus(400) + } + return EditorController.addDoc( + project_id, + parent_folder_id, + name, + [], + 'editor', + user_id, + function(error, doc) { + if (error === 'project_has_to_many_files') { + return res + .status(400) + .json(req.i18n.translate('project_has_to_many_files')) + } else if (error != null) { + return next(error) + } else { + return res.json(doc) + } + } + ) + }, + + addFolder(req, res, next) { + const project_id = req.params.Project_id + const { name } = req.body + const { parent_folder_id } = req.body + if (!EditorHttpController._nameIsAcceptableLength(name)) { + return res.sendStatus(400) + } + return EditorController.addFolder( + project_id, + parent_folder_id, + name, + 'editor', + function(error, doc) { + if (error === 'project_has_to_many_files') { + return res + .status(400) + .json(req.i18n.translate('project_has_to_many_files')) + } else if ( + (error != null ? error.message : undefined) === 'invalid element name' + ) { + return res.status(400).json(req.i18n.translate('invalid_file_name')) + } else if (error != null) { + return next(error) + } else { + return res.json(doc) + } + } + ) + }, + + renameEntity(req, res, next) { + const project_id = req.params.Project_id + const { entity_id } = req.params + const { entity_type } = req.params + const { name } = req.body + if (!EditorHttpController._nameIsAcceptableLength(name)) { + return res.sendStatus(400) + } + const user_id = AuthenticationController.getLoggedInUserId(req) + return EditorController.renameEntity( + project_id, + entity_id, + entity_type, + name, + user_id, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + }, + + moveEntity(req, res, next) { + const project_id = req.params.Project_id + const { entity_id } = req.params + const { entity_type } = req.params + const { folder_id } = req.body + const user_id = AuthenticationController.getLoggedInUserId(req) + return EditorController.moveEntity( + project_id, + entity_id, + folder_id, + entity_type, + user_id, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + }, + + deleteDoc(req, res, next) { + req.params.entity_type = 'doc' + return EditorHttpController.deleteEntity(req, res, next) + }, + + deleteFile(req, res, next) { + req.params.entity_type = 'file' + return EditorHttpController.deleteEntity(req, res, next) + }, + + deleteFolder(req, res, next) { + req.params.entity_type = 'folder' + return EditorHttpController.deleteEntity(req, res, next) + }, + + deleteEntity(req, res, next) { + const project_id = req.params.Project_id + const { entity_id } = req.params + const { entity_type } = req.params + const user_id = AuthenticationController.getLoggedInUserId(req) + return EditorController.deleteEntity( + project_id, + entity_id, + entity_type, + 'editor', + user_id, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + } +} diff --git a/services/web/app/src/Features/Editor/EditorRealTimeController.js b/services/web/app/src/Features/Editor/EditorRealTimeController.js new file mode 100644 index 0000000000..c5152ae0af --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorRealTimeController.js @@ -0,0 +1,43 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let EditorRealTimeController +const Settings = require('settings-sharelatex') +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('realtime') +const os = require('os') +const crypto = require('crypto') + +const HOST = os.hostname() +const RND = crypto.randomBytes(4).toString('hex') // generate a random key for this process +let COUNT = 0 + +module.exports = EditorRealTimeController = { + emitToRoom(room_id, message, ...payload) { + // create a unique message id using a counter + const message_id = `web:${HOST}:${RND}-${COUNT++}` + return rclient.publish( + 'editor-events', + JSON.stringify({ + room_id, + message, + payload, + _id: message_id + }) + ) + }, + + emitToAll(message, ...payload) { + return this.emitToRoom('all', message, ...Array.from(payload)) + } +} diff --git a/services/web/app/src/Features/Editor/EditorRouter.js b/services/web/app/src/Features/Editor/EditorRouter.js new file mode 100644 index 0000000000..5b775af392 --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorRouter.js @@ -0,0 +1,83 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const EditorHttpController = require('./EditorHttpController') +const AuthenticationController = require('../Authentication/AuthenticationController') +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.post( + '/project/:Project_id/doc', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-doc-to-project', + params: ['Project_id'], + maxRequests: 30, + timeInterval: 60 + }), + EditorHttpController.addDoc + ) + webRouter.post( + '/project/:Project_id/folder', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-folder-to-project', + params: ['Project_id'], + maxRequests: 60, + timeInterval: 60 + }), + EditorHttpController.addFolder + ) + + webRouter.post( + '/project/:Project_id/:entity_type/:entity_id/rename', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.renameEntity + ) + webRouter.post( + '/project/:Project_id/:entity_type/:entity_id/move', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.moveEntity + ) + + webRouter.delete( + '/project/:Project_id/file/:entity_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.deleteFile + ) + webRouter.delete( + '/project/:Project_id/doc/:entity_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.deleteDoc + ) + webRouter.delete( + '/project/:Project_id/folder/:entity_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.deleteFolder + ) + + // Called by the real-time API to load up the current project state. + // This is a post request because it's more than just a getting of data. We take actions + // whenever a user joins a project, like updating the deleted status. + return apiRouter.post( + '/project/:Project_id/join', + AuthenticationController.httpAuth, + RateLimiterMiddleware.rateLimit({ + endpointName: 'join-project', + params: ['Project_id'], + maxRequests: 30, + timeInterval: 60 + }), + EditorHttpController.joinProject + ) + } +} diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/src/Features/Email/Bodies/SingleCTAEmailBody.js similarity index 96% rename from services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee rename to services/web/app/src/Features/Email/Bodies/SingleCTAEmailBody.js index eb36869a2c..296f21ce09 100644 --- a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee +++ b/services/web/app/src/Features/Email/Bodies/SingleCTAEmailBody.js @@ -1,7 +1,13 @@ -_ = require("underscore") -settings = require "settings-sharelatex" +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const _ = require('underscore') +const settings = require('settings-sharelatex') -module.exports = _.template """ +module.exports = _.template(`\
<% if (title) { %> @@ -56,5 +62,5 @@ module.exports = _.template """ }) %> -<% } %> -""" +<% } %>\ +`) diff --git a/services/web/app/coffee/Features/Email/Bodies/sl-SingleCTAEmailBody.coffee b/services/web/app/src/Features/Email/Bodies/sl-SingleCTAEmailBody.js similarity index 96% rename from services/web/app/coffee/Features/Email/Bodies/sl-SingleCTAEmailBody.coffee rename to services/web/app/src/Features/Email/Bodies/sl-SingleCTAEmailBody.js index 192f572beb..2a4e2aaa1e 100644 --- a/services/web/app/coffee/Features/Email/Bodies/sl-SingleCTAEmailBody.coffee +++ b/services/web/app/src/Features/Email/Bodies/sl-SingleCTAEmailBody.js @@ -1,7 +1,13 @@ -_ = require("underscore") -settings = require "settings-sharelatex" +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const _ = require('underscore') +const settings = require('settings-sharelatex') -module.exports = _.template """ +module.exports = _.template(`\
<% if (title) { %> @@ -54,5 +60,5 @@ module.exports = _.template """ "description": "<%= gmailGoToAction.description %>" } -<% } %> -""" +<% } %>\ +`) diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js new file mode 100644 index 0000000000..a438c3ad56 --- /dev/null +++ b/services/web/app/src/Features/Email/EmailBuilder.js @@ -0,0 +1,466 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require('underscore') +const settings = require('settings-sharelatex') +const marked = require('marked') +const StringHelper = require('../Helpers/StringHelper') + +const PersonalEmailLayout = require('./Layouts/PersonalEmailLayout') +const NotificationEmailLayout = require('./Layouts/NotificationEmailLayout') +const BaseWithHeaderEmailLayout = require(`./Layouts/${ + settings.brandPrefix +}BaseWithHeaderEmailLayout`) +const SpamSafe = require('./SpamSafe') + +const SingleCTAEmailBody = require(`./Bodies/${ + settings.brandPrefix +}SingleCTAEmailBody`) + +const CTAEmailTemplate = function(content) { + if (content.greeting == null) { + content.greeting = () => 'Hi,' + } + if (content.secondaryMessage == null) { + content.secondaryMessage = () => '' + } + return { + subject(opts) { + return content.subject(opts) + }, + layout: BaseWithHeaderEmailLayout, + plainTextTemplate(opts) { + return `\ +${content.greeting(opts)} + +${content.message(opts).trim()} + +${content.ctaText(opts)}: ${content.ctaURL(opts)} + +${(typeof content.secondaryMessage === 'function' + ? content.secondaryMessage(opts).trim() + : undefined) || ''} + +Regards, +The ${settings.appName} Team - ${settings.siteUrl}\ +` + }, + compiledTemplate(opts) { + return SingleCTAEmailBody({ + title: + typeof content.title === 'function' ? content.title(opts) : undefined, + greeting: content.greeting(opts), + message: marked(content.message(opts).trim()), + secondaryMessage: marked(content.secondaryMessage(opts).trim()), + ctaText: content.ctaText(opts), + ctaURL: content.ctaURL(opts), + gmailGoToAction: + typeof content.gmailGoToAction === 'function' + ? content.gmailGoToAction(opts) + : undefined, + StringHelper + }) + } + } +} + +const templates = {} + +templates.accountMergeToOverleafAddress = CTAEmailTemplate({ + subject() { + return `Confirm Account Merge - ${settings.appName}` + }, + title() { + return 'Confirm Account Merge' + }, + message() { + return `\ +To merge your ShareLaTeX and Overleaf accounts, click the button below. +If you think you have received this message in error, +please contact us at https://www.overleaf.com/contact\ +` + }, + ctaText() { + return 'Confirm Account Merge' + }, + ctaURL(opts) { + return opts.tokenLinkUrl + } +}) + +templates.accountMergeToSharelatexAddress = + templates.accountMergeToOverleafAddress + +templates.registered = CTAEmailTemplate({ + subject() { + return `Activate your ${settings.appName} Account` + }, + message(opts) { + return `\ +Congratulations, you've just had an account created for you on ${ + settings.appName + } with the email address '${_.escape(opts.to)}'. + +Click here to set your password and log in:\ +` + }, + secondaryMessage() { + return `If you have any questions or problems, please contact ${ + settings.adminEmail + }` + }, + ctaText() { + return 'Set password' + }, + ctaURL(opts) { + return opts.setNewPasswordUrl + } +}) + +templates.canceledSubscription = CTAEmailTemplate({ + subject() { + return `${settings.appName} thoughts` + }, + message() { + return `\ +I'm sorry to see you cancelled your ${ + settings.appName + } premium account. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?\ +` + }, + secondaryMessage() { + return 'Thank you in advance!' + }, + ctaText() { + return 'Leave Feedback' + }, + ctaURL(opts) { + return 'https://docs.google.com/forms/d/e/1FAIpQLScqU6Je1r4Afz6ul6oY0RAfN7RabdWv_oL1u7Rj1YBmXS4fiQ/viewform?usp=sf_link' + } +}) + +templates.reactivatedSubscription = CTAEmailTemplate({ + subject() { + return `Subscription Reactivated - ${settings.appName}` + }, + message(opts) { + return `\ +Your subscription was reactivated successfully.\ +` + }, + ctaText() { + return 'View Subscription Dashboard' + }, + ctaURL(opts) { + return `${settings.siteUrl}/user/subscription` + } +}) + +templates.passwordResetRequested = CTAEmailTemplate({ + subject() { + return `Password Reset - ${settings.appName}` + }, + title() { + return 'Password Reset' + }, + message() { + return `We got a request to reset your ${settings.appName} password.` + }, + secondaryMessage() { + return `\ +If you ignore this message, your password won't be changed. + +If you didn't request a password reset, let us know.\ +` + }, + ctaText() { + return 'Reset password' + }, + ctaURL(opts) { + return opts.setNewPasswordUrl + } +}) + +templates.confirmEmail = CTAEmailTemplate({ + subject() { + return `Confirm Email - ${settings.appName}` + }, + title() { + return 'Confirm Email' + }, + message() { + return `Please confirm your email on ${settings.appName}.` + }, + ctaText() { + return 'Confirm Email' + }, + ctaURL(opts) { + return opts.confirmEmailUrl + } +}) + +templates.projectInvite = CTAEmailTemplate({ + subject(opts) { + return `${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'New Project') + )} - shared by ${_.escape( + SpamSafe.safeEmail(opts.owner.email, 'a collaborator') + )}` + }, + title(opts) { + return `${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'New Project') + )} - shared by ${_.escape( + SpamSafe.safeEmail(opts.owner.email, 'a collaborator') + )}` + }, + message(opts) { + return `${_.escape( + SpamSafe.safeEmail(opts.owner.email, 'a collaborator') + )} wants to share ${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'a new project') + )} with you.` + }, + ctaText() { + return 'View project' + }, + ctaURL(opts) { + return opts.inviteUrl + }, + gmailGoToAction(opts) { + return { + target: opts.inviteUrl, + name: 'View project', + description: `Join ${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'project') + )} at ${settings.appName}` + } + } +}) + +templates.verifyEmailToJoinTeam = CTAEmailTemplate({ + subject(opts) { + return `${_.escape( + SpamSafe.safeUserName(opts.inviterName, 'A collaborator') + )} has invited you to join a team on ${settings.appName}` + }, + title(opts) { + return `${_.escape( + SpamSafe.safeUserName(opts.inviterName, 'A collaborator') + )} has invited you to join a team on ${settings.appName}` + }, + message(opts) { + return `Please click the button below to join the team and enjoy the benefits of an upgraded ${ + settings.appName + } account.` + }, + ctaText(opts) { + return 'Join now' + }, + ctaURL(opts) { + return opts.acceptInviteUrl + } +}) + +templates.dropboxUnlinkedDuplicate = CTAEmailTemplate({ + subject() { + return `Your Dropbox Account has been Unlinked - ${settings.appName}` + }, + message(opts) { + return `\ +Our automated systems have detected that your Dropbox account was linked to more than one Overleaf accounts. This should not have been allowed and might be causing issues with the Dropbox sync feature. + +We have now unlinked all your Dropbox and Overleaf Accounts. To ensure your project will keep syncing you can link your Dropbox account to the Overleaf account of your choice now.\ +` + }, + ctaText() { + return 'Link Dropbox Account' + }, + ctaURL(opts) { + return `${settings.siteUrl}/user/settings` + } +}) + +templates.testEmail = CTAEmailTemplate({ + subject() { + return `A Test Email from ${settings.appName}` + }, + title() { + return `A Test Email from ${settings.appName}` + }, + greeting() { + return 'Hi,' + }, + message() { + return `This is a test Email from ${settings.appName}` + }, + ctaText() { + return `Open ${settings.appName}` + }, + ctaURL() { + return settings.siteUrl + } +}) + +templates.projectsTransferredFromSharelatex = CTAEmailTemplate({ + subject() { + return 'ShareLaTeX projects transferred to your Overleaf account' + }, + title() { + return 'ShareLaTeX projects transferred to your Overleaf account' + }, + message(opts) { + return `\ +We are writing with important information about your Overleaf and ShareLaTeX accounts. + +As part of our ongoing work to [integrate Overleaf and ShareLaTeX](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf), +we found a ShareLaTeX account with the email address ${ + opts.to + } that matches your Overleaf account. + +We have now transferred the projects from this ShareLaTeX account into your Overleaf account, so you may notice some new +projects on your Overleaf projects page. + +When you next log in, you may be prompted to reconfirm your email address in order to regain access to your account. +If you have any questions, please contact our support team by reply.\ +` + }, + ctaText() { + return `Log in to ${settings.appName}` + }, + ctaURL() { + return settings.siteUrl + '/login' + } +}) + +templates.emailAddressPoachedEmail = CTAEmailTemplate({ + subject() { + return `One of your email addresses has been moved to another ${ + settings.appName + } account` + }, + title() { + return `One of your email addresses has been moved to another ${ + settings.appName + } account` + }, + message(opts) { + let message = `\ +We are writing with important information about your Overleaf account. + +You added the email address ${opts.poached} to your ${ + opts.to + } Overleaf account as a secondary (or affiliation) +email address, but we have had to remove it. + +This is because your ${ + opts.poached + } email address was also in use as the primary email address for an older Overleaf +account from before our [integration with ShareLaTeX to create Overleaf v2](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf). + +### What do I need to do? + +You now have two Overleaf accounts, one under ${opts.poached} and one under ${ + opts.to + }. + +You may wish to log in to Overleaf as ${ + opts.poached + } to check whether you have projects there that you would like to +keep. If you are not sure of the password, you can send yourself a password reset email to ${ + opts.poached + }, via +https://www.overleaf.com/user/password/reset + +Once you have downloaded your projects, you may wish to delete your +${ + opts.poached + } Overleaf account, which you can do from your account settings. You will then be able to add +${opts.poached} as a secondary email address on your ${opts.to} account again. + +\ +` + if (opts.proFeatures) { + message += `\ +Because your ${ + opts.poached + } email address was an institutional affiliation through which you had Pro features. Your Pro +features have been transferred to your ${ + opts.poached + } account. If you would like to transfer them back to your +${opts.to} account, you will need to delete the ${ + opts.poached + } account and re-add it as a secondary email address, +as described above. + +\ +` + } + + message += `\ +If you have any questions, you can contact our support team by reply.\ +` + return message + }, + ctaText() { + return `Log in to ${settings.appName}` + }, + ctaURL() { + return settings.siteUrl + '/login' + } +}) + +module.exports = { + templates, + CTAEmailTemplate, + buildEmail(templateName, opts) { + const template = templates[templateName] + opts.siteUrl = settings.siteUrl + opts.body = template.compiledTemplate(opts) + if ( + __guard__( + settings.email != null ? settings.email.templates : undefined, + x => x.customFooter + ) != null + ) { + opts.body += __guard__( + settings.email != null ? settings.email.templates : undefined, + x1 => x1.customFooter + ) + } + return { + subject: template.subject(opts), + html: template.layout(opts), + text: __guardMethod__(template, 'plainTextTemplate', o => + o.plainTextTemplate(opts) + ) + } + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} +function __guardMethod__(obj, methodName, transform) { + if ( + typeof obj !== 'undefined' && + obj !== null && + typeof obj[methodName] === 'function' + ) { + return transform(obj, methodName) + } else { + return undefined + } +} diff --git a/services/web/app/src/Features/Email/EmailHandler.js b/services/web/app/src/Features/Email/EmailHandler.js new file mode 100644 index 0000000000..938cd62a96 --- /dev/null +++ b/services/web/app/src/Features/Email/EmailHandler.js @@ -0,0 +1,34 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('settings-sharelatex') +const EmailBuilder = require('./EmailBuilder') +const EmailSender = require('./EmailSender') + +if (settings.email == null) { + settings.email = { lifecycleEnabled: false } +} + +module.exports = { + sendEmail(emailType, opts, callback) { + if (callback == null) { + callback = function(err) {} + } + const email = EmailBuilder.buildEmail(emailType, opts) + if (email.type === 'lifecycle' && !settings.email.lifecycle) { + return callback() + } + opts.html = email.html + opts.text = email.text + opts.subject = email.subject + return EmailSender.sendEmail(opts, err => callback(err)) + } +} diff --git a/services/web/app/src/Features/Email/EmailSender.js b/services/web/app/src/Features/Email/EmailSender.js new file mode 100644 index 0000000000..8166fc618d --- /dev/null +++ b/services/web/app/src/Features/Email/EmailSender.js @@ -0,0 +1,208 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let defaultFromAddress, nm_client +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const Settings = require('settings-sharelatex') +const nodemailer = require('nodemailer') +const sesTransport = require('nodemailer-ses-transport') +const sgTransport = require('nodemailer-sendgrid-transport') +const mandrillTransport = require('nodemailer-mandrill-transport') +const rateLimiter = require('../../infrastructure/RateLimiter') +const _ = require('underscore') + +if (Settings.email != null && Settings.email.fromAddress != null) { + defaultFromAddress = Settings.email.fromAddress +} else { + defaultFromAddress = '' +} + +// provide dummy mailer unless we have a better one configured. +let client = { + sendMail(options, callback) { + if (callback == null) { + callback = function(err, status) {} + } + logger.log({ options }, 'Would send email if enabled.') + return callback() + } +} +if ( + __guard__( + __guard__( + Settings != null ? Settings.email : undefined, + x1 => x1.parameters + ), + x => x.AWSAccessKeyID + ) != null || + __guard__(Settings != null ? Settings.email : undefined, x2 => x2.driver) === + 'ses' +) { + logger.log('using aws ses for email') + nm_client = nodemailer.createTransport( + sesTransport(Settings.email.parameters) + ) +} else if ( + __guard__( + __guard__( + Settings != null ? Settings.email : undefined, + x4 => x4.parameters + ), + x3 => x3.sendgridApiKey + ) != null +) { + logger.log('using sendgrid for email') + nm_client = nodemailer.createTransport( + sgTransport({ + auth: { + api_key: __guard__( + __guard__( + Settings != null ? Settings.email : undefined, + x6 => x6.parameters + ), + x5 => x5.sendgridApiKey + ) + } + }) + ) +} else if ( + __guard__( + __guard__( + Settings != null ? Settings.email : undefined, + x8 => x8.parameters + ), + x7 => x7.MandrillApiKey + ) != null +) { + logger.log('using mandril for email') + nm_client = nodemailer.createTransport( + mandrillTransport({ + auth: { + apiKey: __guard__( + __guard__( + Settings != null ? Settings.email : undefined, + x10 => x10.parameters + ), + x9 => x9.MandrillApiKey + ) + } + }) + ) +} else if ( + __guard__( + Settings != null ? Settings.email : undefined, + x11 => x11.parameters + ) != null +) { + logger.log('using smtp for email') + const smtp = _.pick( + __guard__( + Settings != null ? Settings.email : undefined, + x12 => x12.parameters + ), + 'host', + 'port', + 'secure', + 'auth', + 'ignoreTLS' + ) + nm_client = nodemailer.createTransport(smtp) +} else { + logger.warn( + 'Email transport and/or parameters not defined. No emails will be sent.' + ) + nm_client = client +} + +if (nm_client != null) { + client = nm_client +} else { + logger.warn( + 'Failed to create email transport. Please check your settings. No email will be sent.' + ) +} + +const checkCanSendEmail = function(options, callback) { + if (options.sendingUser_id == null) { + // email not sent from user, not rate limited + return callback(null, true) + } + const opts = { + endpointName: 'send_email', + timeInterval: 60 * 60 * 3, + subjectName: options.sendingUser_id, + throttle: 100 + } + return rateLimiter.addCount(opts, callback) +} + +module.exports = { + sendEmail(options, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { receiver: options.to, subject: options.subject }, + 'sending email' + ) + return checkCanSendEmail(options, function(err, canContinue) { + if (err != null) { + return callback(err) + } + if (!canContinue) { + logger.log( + { + sendingUser_id: options.sendingUser_id, + to: options.to, + subject: options.subject, + canContinue + }, + 'rate limit hit for sending email, not sending' + ) + return callback('rate limit hit sending email') + } + metrics.inc('email') + options = { + to: options.to, + from: defaultFromAddress, + subject: options.subject, + html: options.html, + text: options.text, + replyTo: options.replyTo || Settings.email.replyToAddress, + socketTimeout: 30 * 1000 + } + if (Settings.email.textEncoding != null) { + opts.textEncoding = textEncoding + } + return client.sendMail(options, function(err, res) { + if (err != null) { + logger.err({ err }, 'error sending message') + err = new Error('Cannot send email') + } else { + logger.log(`Message sent to ${options.to}`) + } + return callback(err) + }) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee b/services/web/app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout.js similarity index 96% rename from services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee rename to services/web/app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout.js index 280eb2da16..615ef06893 100644 --- a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee +++ b/services/web/app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout.js @@ -1,7 +1,12 @@ -_ = require("underscore") -settings = require "settings-sharelatex" +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const _ = require('underscore') +const settings = require('settings-sharelatex') -module.exports = _.template """ +module.exports = _.template(`\ @@ -364,7 +369,11 @@ module.exports = _.template """
 

- #{ settings.appName} • #{ settings.siteUrl } + ${settings.appName} • ${ + settings.siteUrl +}

@@ -376,5 +385,5 @@ module.exports = _.template """
                                                           
- -""" +\ +`) diff --git a/services/web/app/coffee/Features/Email/Layouts/NotificationEmailLayout.coffee b/services/web/app/src/Features/Email/Layouts/NotificationEmailLayout.js similarity index 97% rename from services/web/app/coffee/Features/Email/Layouts/NotificationEmailLayout.coffee rename to services/web/app/src/Features/Email/Layouts/NotificationEmailLayout.js index 295951aa8c..dd16f96fbc 100644 --- a/services/web/app/coffee/Features/Email/Layouts/NotificationEmailLayout.coffee +++ b/services/web/app/src/Features/Email/Layouts/NotificationEmailLayout.js @@ -1,7 +1,13 @@ -_ = require("underscore") -settings = require "settings-sharelatex" +/* eslint-disable + max-len, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const _ = require('underscore') +const settings = require('settings-sharelatex') -module.exports = _.template """ +module.exports = _.template(`\ @@ -313,7 +319,7 @@ module.exports = _.template """
- #{settings.appName} + ${settings.appName}
@@ -351,5 +357,5 @@ module.exports = _.template """ - -""" \ No newline at end of file +\ +`) diff --git a/services/web/app/coffee/Features/Email/Layouts/PersonalEmailLayout.coffee b/services/web/app/src/Features/Email/Layouts/PersonalEmailLayout.js similarity index 65% rename from services/web/app/coffee/Features/Email/Layouts/PersonalEmailLayout.coffee rename to services/web/app/src/Features/Email/Layouts/PersonalEmailLayout.js index 839b5cbd6a..9b1e132c68 100644 --- a/services/web/app/coffee/Features/Email/Layouts/PersonalEmailLayout.coffee +++ b/services/web/app/src/Features/Email/Layouts/PersonalEmailLayout.js @@ -1,7 +1,8 @@ -_ = require("underscore") +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +const _ = require('underscore') - -module.exports = _.template ''' +module.exports = _.template(`\ @@ -14,5 +15,5 @@ module.exports = _.template ''' <%= body %> - -''' \ No newline at end of file +\ +`) diff --git a/services/web/app/coffee/Features/Email/Layouts/sl-BaseWithHeaderEmailLayout.coffee b/services/web/app/src/Features/Email/Layouts/sl-BaseWithHeaderEmailLayout.js similarity index 96% rename from services/web/app/coffee/Features/Email/Layouts/sl-BaseWithHeaderEmailLayout.coffee rename to services/web/app/src/Features/Email/Layouts/sl-BaseWithHeaderEmailLayout.js index d141f64ff7..f678e33189 100644 --- a/services/web/app/coffee/Features/Email/Layouts/sl-BaseWithHeaderEmailLayout.coffee +++ b/services/web/app/src/Features/Email/Layouts/sl-BaseWithHeaderEmailLayout.js @@ -1,7 +1,12 @@ -_ = require("underscore") -settings = require "settings-sharelatex" +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const _ = require('underscore') +const settings = require('settings-sharelatex') -module.exports = _.template """ +module.exports = _.template(`\ @@ -361,7 +366,11 @@ module.exports = _.template """
 

- #{ settings.appName} • #{ settings.siteUrl } + ${settings.appName} • ${ + settings.siteUrl +}

@@ -373,5 +382,5 @@ module.exports = _.template """
                                                           
- -""" +\ +`) diff --git a/services/web/app/src/Features/Email/SpamSafe.js b/services/web/app/src/Features/Email/SpamSafe.js new file mode 100644 index 0000000000..aaf73f915d --- /dev/null +++ b/services/web/app/src/Features/Email/SpamSafe.js @@ -0,0 +1,66 @@ +/* eslint-disable + chai-friendly/no-unused-expressions, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const XRegExp = require('xregexp') + +// A note about SAFE_REGEX: +// We have to escape the escape characters because XRegExp compiles it first. +// So it's equivalent to `^[\p{L}\p{N}\s\-_!&\(\)]+$] +// \p{L} = any letter in any language +// \p{N} = any kind of numeric character +// https://www.regular-expressions.info/unicode.html#prop is a good resource for +// more obscure regex features. standard RegExp does not support these + +const SAFE_REGEX = XRegExp("^[\\p{L}\\p{N}\\s\\-_!'&\\(\\)]+$") +const EMAIL_REGEX = XRegExp('^[\\p{L}\\p{N}.+_-]+@[\\w.-]+$') + +var SpamSafe = { + isSafeUserName(name) { + return SAFE_REGEX.test(name) && name.length <= 30 + }, + + isSafeProjectName(name) { + if (XRegExp('\\p{Han}').test(name)) { + SAFE_REGEX.test(name) && name.length <= 30 + } + return SAFE_REGEX.test(name) && name.length <= 100 + }, + + isSafeEmail(email) { + return EMAIL_REGEX.test(email) && email.length <= 40 + }, + + safeUserName(name, alternative, project) { + if (project == null) { + project = false + } + if (SpamSafe.isSafeUserName(name)) { + return name + } + return alternative + }, + + safeProjectName(name, alternative) { + if (SpamSafe.isSafeProjectName(name)) { + return name + } + return alternative + }, + + safeEmail(email, alternative) { + if (SpamSafe.isSafeEmail(email)) { + return email + } + return alternative + } +} + +module.exports = SpamSafe diff --git a/services/web/app/src/Features/Errors/ErrorController.js b/services/web/app/src/Features/Errors/ErrorController.js new file mode 100644 index 0000000000..994de3b5fb --- /dev/null +++ b/services/web/app/src/Features/Errors/ErrorController.js @@ -0,0 +1,91 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ErrorController +const Errors = require('./Errors') +const logger = require('logger-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = ErrorController = { + notFound(req, res) { + res.status(404) + return res.render('general/404', { title: 'page_not_found' }) + }, + + forbidden(req, res) { + res.status(403) + return res.render('user/restricted') + }, + + serverError(req, res) { + res.status(500) + return res.render('general/500', { title: 'Server Error' }) + }, + + accountMergeError(req, res) { + res.status(500) + return res.render('general/account-merge-error', { + title: 'Account Access Error' + }) + }, + + handleError(error, req, res, next) { + const user = AuthenticationController.getSessionUser(req) + if ((error != null ? error.code : undefined) === 'EBADCSRFTOKEN') { + logger.warn( + { err: error, url: req.url, method: req.method, user }, + 'invalid csrf' + ) + res.sendStatus(403) + return + } + if (error instanceof Errors.NotFoundError) { + logger.warn({ err: error, url: req.url }, 'not found error') + return ErrorController.notFound(req, res) + } else if (error instanceof Errors.ForbiddenError) { + logger.error({ err: error }, 'forbidden error') + return ErrorController.forbidden(req, res) + } else if (error instanceof Errors.TooManyRequestsError) { + logger.warn({ err: error, url: req.url }, 'too many requests error') + return res.sendStatus(429) + } else if (error instanceof Errors.InvalidError) { + logger.warn({ err: error, url: req.url }, 'invalid error') + res.status(400) + return res.send(error.message) + } else if (error instanceof Errors.InvalidNameError) { + logger.warn({ err: error, url: req.url }, 'invalid name error') + res.status(400) + return res.send(error.message) + } else if (error instanceof Errors.AccountMergeError) { + logger.error({ err: error }, 'account merge error') + return ErrorController.accountMergeError(req, res) + } else { + logger.error( + { err: error, url: req.url, method: req.method, user }, + 'error passed to top level next middleware' + ) + return ErrorController.serverError(req, res) + } + }, + + handleApiError(error, req, res, next) { + if (error instanceof Errors.NotFoundError) { + logger.warn({ err: error, url: req.url }, 'not found error') + return res.sendStatus(404) + } else { + logger.error( + { err: error, url: req.url, method: req.method }, + 'error passed to top level next middleware' + ) + return res.sendStatus(500) + } + } +} diff --git a/services/web/app/src/Features/Errors/Errors.js b/services/web/app/src/Features/Errors/Errors.js new file mode 100644 index 0000000000..73c334e729 --- /dev/null +++ b/services/web/app/src/Features/Errors/Errors.js @@ -0,0 +1,183 @@ +/* eslint-disable + max-len, + no-proto, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Errors +var NotFoundError = function(message) { + const error = new Error(message) + error.name = 'NotFoundError' + error.__proto__ = NotFoundError.prototype + return error +} +NotFoundError.prototype.__proto__ = Error.prototype + +var ForbiddenError = function(message) { + const error = new Error(message) + error.name = 'ForbiddenError' + error.__proto__ = ForbiddenError.prototype + return error +} +ForbiddenError.prototype.__proto__ = Error.prototype + +var ServiceNotConfiguredError = function(message) { + const error = new Error(message) + error.name = 'ServiceNotConfiguredError' + error.__proto__ = ServiceNotConfiguredError.prototype + return error +} +ServiceNotConfiguredError.prototype.__proto__ = Error.prototype + +var TooManyRequestsError = function(message) { + const error = new Error(message) + error.name = 'TooManyRequestsError' + error.__proto__ = TooManyRequestsError.prototype + return error +} +TooManyRequestsError.prototype.__proto__ = Error.prototype + +var InvalidNameError = function(message) { + const error = new Error(message) + error.name = 'InvalidNameError' + error.__proto__ = InvalidNameError.prototype + return error +} +InvalidNameError.prototype.__proto__ = Error.prototype + +var UnsupportedFileTypeError = function(message) { + const error = new Error(message) + error.name = 'UnsupportedFileTypeError' + error.__proto__ = UnsupportedFileTypeError.prototype + return error +} +UnsupportedFileTypeError.prototype.__proto__ = Error.prototype + +var UnsupportedExportRecordsError = function(message) { + const error = new Error(message) + error.name = 'UnsupportedExportRecordsError' + error.__proto__ = UnsupportedExportRecordsError.prototype + return error +} +UnsupportedExportRecordsError.prototype.__proto__ = Error.prototype + +var V1HistoryNotSyncedError = function(message) { + const error = new Error(message) + error.name = 'V1HistoryNotSyncedError' + error.__proto__ = V1HistoryNotSyncedError.prototype + return error +} +V1HistoryNotSyncedError.prototype.__proto__ = Error.prototype + +var ProjectHistoryDisabledError = function(message) { + const error = new Error(message) + error.name = 'ProjectHistoryDisabledError' + error.__proto__ = ProjectHistoryDisabledError.prototype + return error +} +ProjectHistoryDisabledError.prototype.__proto__ = Error.prototype + +var V1ConnectionError = function(message) { + const error = new Error(message) + error.name = 'V1ConnectionError' + error.__proto__ = V1ConnectionError.prototype + return error +} +V1ConnectionError.prototype.__proto__ = Error.prototype + +var UnconfirmedEmailError = function(message) { + const error = new Error(message) + error.name = 'UnconfirmedEmailError' + error.__proto__ = UnconfirmedEmailError.prototype + return error +} +UnconfirmedEmailError.prototype.__proto__ = Error.prototype + +var EmailExistsError = function(message) { + const error = new Error(message) + error.name = 'EmailExistsError' + error.__proto__ = EmailExistsError.prototype + return error +} +EmailExistsError.prototype.__proto__ = Error.prototype + +var InvalidError = function(message) { + const error = new Error(message) + error.name = 'InvalidError' + error.__proto__ = InvalidError.prototype + return error +} +InvalidError.prototype.__proto__ = Error.prototype + +var AccountMergeError = function(message) { + const error = new Error(message) + error.name = 'AccountMergeError' + error.__proto__ = AccountMergeError.prototype + return error +} +AccountMergeError.prototype.__proto__ = Error.prototype + +var NotInV2Error = function(message) { + const error = new Error(message) + error.name = 'NotInV2Error' + error.__proto__ = NotInV2Error.prototype + return error +} +NotInV2Error.prototype.__proto__ = Error.prototype + +var SLInV2Error = function(message) { + const error = new Error(message) + error.name = 'SLInV2Error' + error.__proto__ = SLInV2Error.prototype + return error +} +SLInV2Error.prototype.__proto__ = Error.prototype + +const ThirdPartyUserNotFoundError = function(message) { + if (message == null) { + message = 'user not found for provider and external id' + } + const error = new Error(message) + error.name = 'ThirdPartyUserNotFoundError' + error.__proto__ = SLInV2Error.prototype + return error +} +ThirdPartyUserNotFoundError.prototype.__proto__ = Error.prototype + +var SubscriptionAdminDeletionError = function(message) { + if (message == null) { + message = 'subscription admins cannot be deleted' + } + const error = new Error(message) + error.name = 'SubscriptionAdminDeletionError' + error.__proto__ = SubscriptionAdminDeletionError.prototype + return error +} +SubscriptionAdminDeletionError.prototype.__proto__ = Error.prototype + +module.exports = Errors = { + NotFoundError, + ForbiddenError, + ServiceNotConfiguredError, + TooManyRequestsError, + InvalidNameError, + UnsupportedFileTypeError, + UnsupportedExportRecordsError, + V1HistoryNotSyncedError, + ProjectHistoryDisabledError, + V1ConnectionError, + UnconfirmedEmailError, + EmailExistsError, + InvalidError, + AccountMergeError, + NotInV2Error, + SLInV2Error, + ThirdPartyUserNotFoundError, + SubscriptionAdminDeletionError +} diff --git a/services/web/app/src/Features/Exports/ExportsController.js b/services/web/app/src/Features/Exports/ExportsController.js new file mode 100644 index 0000000000..5c81d3e190 --- /dev/null +++ b/services/web/app/src/Features/Exports/ExportsController.js @@ -0,0 +1,123 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ExportsHandler = require('./ExportsHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const logger = require('logger-sharelatex') + +module.exports = { + exportProject(req, res, next) { + const { project_id, brand_variation_id } = req.params + const user_id = AuthenticationController.getLoggedInUserId(req) + const export_params = { + project_id, + brand_variation_id, + user_id + } + + if (req.body) { + if (req.body.firstName) { + export_params.first_name = req.body.firstName.trim() + } + if (req.body.lastName) { + export_params.last_name = req.body.lastName.trim() + } + // additional parameters for gallery exports + if (req.body.title) { + export_params.title = req.body.title.trim() + } + if (req.body.description) { + export_params.description = req.body.description.trim() + } + if (req.body.author) { + export_params.author = req.body.author.trim() + } + if (req.body.license) { + export_params.license = req.body.license.trim() + } + if (req.body.showSource != null) { + export_params.show_source = req.body.showSource + } + } + + return ExportsHandler.exportProject(export_params, function( + err, + export_data + ) { + if (err != null) { + if (err.forwardResponse != null) { + logger.log( + { responseError: err.forwardResponse }, + 'forwarding response' + ) + const statusCode = err.forwardResponse.status || 500 + return res.status(statusCode).json(err.forwardResponse) + } else { + return next(err) + } + } + logger.log( + { + user_id, + project_id, + brand_variation_id, + export_v1_id: export_data.v1_id + }, + 'exported project' + ) + return res.json({ export_v1_id: export_data.v1_id }) + }) + }, + + exportStatus(req, res) { + const { export_id } = req.params + return ExportsHandler.fetchExport(export_id, function(err, export_json) { + let json + if (err != null) { + json = { + status_summary: 'failed', + status_detail: err.toString + } + res.json({ export_json: json }) + return err + } + const parsed_export = JSON.parse(export_json) + json = { + status_summary: parsed_export.status_summary, + status_detail: parsed_export.status_detail, + partner_submission_id: parsed_export.partner_submission_id, + v2_user_email: parsed_export.v2_user_email, + v2_user_first_name: parsed_export.v2_user_first_name, + v2_user_last_name: parsed_export.v2_user_last_name, + title: parsed_export.title, + token: parsed_export.token + } + return res.json({ export_json: json }) + }) + }, + + exportDownload(req, res, next) { + const { type, export_id } = req.params + + AuthenticationController.getLoggedInUserId(req) + return ExportsHandler.fetchDownload(export_id, type, function( + err, + export_file_url + ) { + if (err != null) { + return next(err) + } + + return res.redirect(export_file_url) + }) + } +} diff --git a/services/web/app/src/Features/Exports/ExportsHandler.js b/services/web/app/src/Features/Exports/ExportsHandler.js new file mode 100644 index 0000000000..5bd72e7acd --- /dev/null +++ b/services/web/app/src/Features/Exports/ExportsHandler.js @@ -0,0 +1,307 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ExportsHandler, self +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectHistoryHandler = require('../Project/ProjectHistoryHandler') +const ProjectLocator = require('../Project/ProjectLocator') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const UserGetter = require('../User/UserGetter') +const logger = require('logger-sharelatex') +let settings = require('settings-sharelatex') +const async = require('async') +let request = require('request') +request = request.defaults() +settings = require('settings-sharelatex') + +module.exports = ExportsHandler = self = { + exportProject(export_params, callback) { + if (callback == null) { + callback = function(error, export_data) {} + } + return self._buildExport(export_params, function(err, export_data) { + if (err != null) { + return callback(err) + } + return self._requestExport(export_data, function(err, export_v1_id) { + if (err != null) { + return callback(err) + } + export_data.v1_id = export_v1_id + // TODO: possibly store the export data in Mongo + return callback(null, export_data) + }) + }) + }, + + _buildExport(export_params, callback) { + if (callback == null) { + callback = function(err, export_data) {} + } + const { + project_id, + user_id, + brand_variation_id, + title, + description, + author, + license, + show_source + } = export_params + const jobs = { + project(cb) { + return ProjectGetter.getProject(project_id, cb) + }, + // TODO: when we update async, signature will change from (cb, results) to (results, cb) + rootDoc: [ + 'project', + (cb, results) => + ProjectRootDocManager.ensureRootDocumentIsValid(project_id, function( + error + ) { + if (error != null) { + return callback(error) + } + return ProjectLocator.findRootDoc( + { project: results.project, project_id }, + cb + ) + }) + ], + user(cb) { + return UserGetter.getUser( + user_id, + { first_name: 1, last_name: 1, email: 1, overleaf: 1 }, + cb + ) + }, + historyVersion(cb) { + return ProjectHistoryHandler.ensureHistoryExistsForProject( + project_id, + function(error) { + if (error != null) { + return callback(error) + } + return self._requestVersion(project_id, cb) + } + ) + } + } + + return async.auto(jobs, function(err, results) { + if (err != null) { + logger.err( + { err, project_id, user_id, brand_variation_id }, + 'error building project export' + ) + return callback(err) + } + + const { project, rootDoc, user, historyVersion } = results + if (rootDoc[1] == null) { + err = new Error('cannot export project without root doc') + logger.err({ err, project_id }) + return callback(err) + } + + if (export_params.first_name && export_params.last_name) { + user.first_name = export_params.first_name + user.last_name = export_params.last_name + } + + const export_data = { + project: { + id: project_id, + rootDocPath: rootDoc[1] != null ? rootDoc[1].fileSystem : undefined, + historyId: __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ), + historyVersion, + v1ProjectId: + project.overleaf != null ? project.overleaf.id : undefined, + metadata: { + compiler: project.compiler, + imageName: project.imageName, + title, + description, + author, + license, + showSource: show_source + } + }, + user: { + id: user_id, + firstName: user.first_name, + lastName: user.last_name, + email: user.email, + orcidId: null, // until v2 gets ORCID + v1UserId: user.overleaf != null ? user.overleaf.id : undefined + }, + destination: { + brandVariationId: brand_variation_id + }, + options: { + callbackUrl: null + } // for now, until we want v1 to call us back + } + return callback(null, export_data) + }) + }, + + _requestExport(export_data, callback) { + if (callback == null) { + callback = function(err, export_v1_id) {} + } + return request.post( + { + url: `${settings.apis.v1.url}/api/v1/sharelatex/exports`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + json: export_data + }, + function(err, res, body) { + if (err != null) { + logger.err( + { err, export: export_data }, + 'error making request to v1 export' + ) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body.exportId) + } else { + logger.err( + { export: export_data }, + `v1 export returned failure; forwarding: ${body}` + ) + // pass the v1 error along for the publish modal to handle + return callback({ forwardResponse: body }) + } + } + ) + }, + + _requestVersion(project_id, callback) { + if (callback == null) { + callback = function(err, export_v1_id) {} + } + return request.get( + { + url: `${ + settings.apis.project_history.url + }/project/${project_id}/version`, + json: true + }, + function(err, res, body) { + if (err != null) { + logger.err( + { err, project_id }, + 'error making request to project history' + ) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body.version) + } else { + err = new Error( + `project history version returned a failure status code: ${ + res.statusCode + }` + ) + logger.err( + { err, project_id }, + `project history version returned failure status code: ${ + res.statusCode + }` + ) + return callback(err) + } + } + ) + }, + + fetchExport(export_id, callback) { + if (callback == null) { + callback = function(err, export_json) {} + } + return request.get( + { + url: `${settings.apis.v1.url}/api/v1/sharelatex/exports/${export_id}`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } + }, + function(err, res, body) { + if (err != null) { + logger.err( + { err, export: export_id }, + 'error making request to v1 export' + ) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body) + } else { + err = new Error( + `v1 export returned a failure status code: ${res.statusCode}` + ) + logger.err( + { err, export: export_id }, + `v1 export returned failure status code: ${res.statusCode}` + ) + return callback(err) + } + } + ) + }, + + fetchDownload(export_id, type, callback) { + if (callback == null) { + callback = function(err, file_url) {} + } + return request.get( + { + url: `${ + settings.apis.v1.url + }/api/v1/sharelatex/exports/${export_id}/${type}_url`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass } + }, + function(err, res, body) { + if (err != null) { + logger.err( + { err, export: export_id }, + 'error making request to v1 export' + ) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body) + } else { + err = new Error( + `v1 export returned a failure status code: ${res.statusCode}` + ) + logger.err( + { err, export: export_id }, + `v1 export zip fetch returned failure status code: ${ + res.statusCode + }` + ) + return callback(err) + } + } + ) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/FileStore/FileHashManager.js b/services/web/app/src/Features/FileStore/FileHashManager.js new file mode 100644 index 0000000000..f4865ae686 --- /dev/null +++ b/services/web/app/src/Features/FileStore/FileHashManager.js @@ -0,0 +1,61 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FileHashManager +const crypto = require('crypto') +const logger = require('logger-sharelatex') +const fs = require('fs') +const _ = require('underscore') + +module.exports = FileHashManager = { + computeHash(filePath, callback) { + if (callback == null) { + callback = function(error, hashValue) {} + } + callback = _.once(callback) // avoid double callbacks + + // taken from v1/history/storage/lib/blob_hash.js + const getGitBlobHeader = byteLength => `blob ${byteLength}` + '\x00' + + const getByteLengthOfFile = cb => + fs.stat(filePath, function(err, stats) { + if (err != null) { + return cb(err) + } + return cb(null, stats.size) + }) + + return getByteLengthOfFile(function(err, byteLength) { + if (err != null) { + return callback(err) + } + + const input = fs.createReadStream(filePath) + input.on('error', function(err) { + logger.err({ filePath, err }, 'error opening file in computeHash') + return callback(err) + }) + + const hash = crypto.createHash('sha1') + hash.setEncoding('hex') + hash.update(getGitBlobHeader(byteLength)) + hash.on('readable', function() { + const result = hash.read() + if (result != null) { + return callback(null, result.toString('hex')) + } + }) + return input.pipe(hash) + }) + } +} diff --git a/services/web/app/src/Features/FileStore/FileStoreController.js b/services/web/app/src/Features/FileStore/FileStoreController.js new file mode 100644 index 0000000000..17e8ab1fce --- /dev/null +++ b/services/web/app/src/Features/FileStore/FileStoreController.js @@ -0,0 +1,76 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const logger = require('logger-sharelatex') +const FileStoreHandler = require('./FileStoreHandler') +const ProjectLocator = require('../Project/ProjectLocator') +const _ = require('underscore') + +const is_mobile_safari = user_agent => + user_agent && + (user_agent.indexOf('iPhone') >= 0 || user_agent.indexOf('iPad') >= 0) + +const is_html = function(file) { + const ends_with = ext => + file.name != null && + file.name.length > ext.length && + file.name.lastIndexOf(ext) === file.name.length - ext.length + + return ends_with('.html') || ends_with('.htm') || ends_with('.xhtml') +} + +module.exports = { + getFile(req, res) { + const project_id = req.params.Project_id + const file_id = req.params.File_id + const queryString = req.query + const user_agent = req.get('User-Agent') + logger.log({ project_id, file_id, queryString }, 'file download') + return ProjectLocator.findElement( + { project_id, element_id: file_id, type: 'file' }, + function(err, file) { + if (err != null) { + logger.err( + { err, project_id, file_id, queryString }, + 'error finding element for downloading file' + ) + return res.sendStatus(500) + } + return FileStoreHandler.getFileStream( + project_id, + file_id, + queryString, + function(err, stream) { + if (err != null) { + logger.err( + { err, project_id, file_id, queryString }, + 'error getting file stream for downloading file' + ) + return res.sendStatus(500) + } + // mobile safari will try to render html files, prevent this + if (is_mobile_safari(user_agent) && is_html(file)) { + logger.log( + { filename: file.name, user_agent }, + 'sending html file to mobile-safari as plain text' + ) + res.setHeader('Content-Type', 'text/plain') + } + res.setContentDisposition('attachment', { filename: file.name }) + return stream.pipe(res) + } + ) + } + ) + } +} diff --git a/services/web/app/src/Features/FileStore/FileStoreHandler.js b/services/web/app/src/Features/FileStore/FileStoreHandler.js new file mode 100644 index 0000000000..f853dfa40e --- /dev/null +++ b/services/web/app/src/Features/FileStore/FileStoreHandler.js @@ -0,0 +1,238 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FileStoreHandler +const logger = require('logger-sharelatex') +const fs = require('fs') +const request = require('request') +const settings = require('settings-sharelatex') +const Async = require('async') +const FileHashManager = require('./FileHashManager') +const { File } = require('../../models/File') + +const oneMinInMs = 60 * 1000 +const fiveMinsInMs = oneMinInMs * 5 + +module.exports = FileStoreHandler = { + RETRY_ATTEMPTS: 3, + + uploadFileFromDisk(project_id, file_args, fsPath, callback) { + if (callback == null) { + callback = function(error, url, fileRef) {} + } + return fs.lstat(fsPath, function(err, stat) { + if (err != null) { + logger.err({ err, project_id, file_args, fsPath }, 'error stating file') + callback(err) + } + if (stat == null) { + logger.err( + { project_id, file_args, fsPath }, + 'stat is not available, can not check file from disk' + ) + return callback(new Error('error getting stat, not available')) + } + if (!stat.isFile()) { + logger.log( + { project_id, file_args, fsPath }, + 'tried to upload symlink, not contining' + ) + return callback(new Error('can not upload symlink')) + } + return Async.retry( + FileStoreHandler.RETRY_ATTEMPTS, + (cb, results) => + FileStoreHandler._doUploadFileFromDisk( + project_id, + file_args, + fsPath, + cb + ), + function(err, result) { + if (err != null) { + logger.err( + { err, project_id, file_args }, + 'Error uploading file, retries failed' + ) + return callback(err) + } + return callback(err, result.url, result.fileRef) + } + ) + }) + }, + + _doUploadFileFromDisk(project_id, file_args, fsPath, callback) { + if (callback == null) { + callback = function(err, result) {} + } + const _cb = callback + callback = function(err, ...result) { + callback = function() {} // avoid double callbacks + return _cb(err, ...Array.from(result)) + } + + return FileHashManager.computeHash(fsPath, function(err, hashValue) { + if (err != null) { + return callback(err) + } + const fileRef = new File( + Object.assign({}, file_args, { hash: hashValue }) + ) + const file_id = fileRef._id + logger.log( + { project_id, file_id, fsPath, hash: hashValue, fileRef }, + 'uploading file from disk' + ) + const readStream = fs.createReadStream(fsPath) + readStream.on('error', function(err) { + logger.err( + { err, project_id, file_id, fsPath }, + 'something went wrong on the read stream of uploadFileFromDisk' + ) + return callback(err) + }) + return readStream.on('open', function() { + const url = FileStoreHandler._buildUrl(project_id, file_id) + const opts = { + method: 'post', + uri: url, + timeout: fiveMinsInMs, + headers: { + 'X-File-Hash-From-Web': hashValue + } // send the hash to the filestore as a custom header so it can be checked + } + const writeStream = request(opts) + writeStream.on('error', function(err) { + logger.err( + { err, project_id, file_id, fsPath }, + 'something went wrong on the write stream of uploadFileFromDisk' + ) + return callback(err) + }) + writeStream.on('response', function(response) { + if (![200, 201].includes(response.statusCode)) { + err = new Error( + `non-ok response from filestore for upload: ${ + response.statusCode + }` + ) + logger.err( + { err, statusCode: response.statusCode }, + 'error uploading to filestore' + ) + return callback(err) + } else { + return callback(null, { url, fileRef }) + } + }) // have to pass back an object because async.retry only accepts a single result argument + return readStream.pipe(writeStream) + }) + }) + }, + + getFileStream(project_id, file_id, query, callback) { + logger.log( + { project_id, file_id, query }, + 'getting file stream from file store' + ) + let queryString = '' + if (query != null && query['format'] != null) { + queryString = `?format=${query['format']}` + } + const opts = { + method: 'get', + uri: `${this._buildUrl(project_id, file_id)}${queryString}`, + timeout: fiveMinsInMs, + headers: {} + } + if (query != null && query['range'] != null) { + const rangeText = query['range'] + if (rangeText && rangeText.match != null && rangeText.match(/\d+-\d+/)) { + opts.headers['range'] = `bytes=${query['range']}` + } + } + const readStream = request(opts) + readStream.on('error', err => + logger.err( + { err, project_id, file_id, query, opts }, + 'error in file stream' + ) + ) + return callback(null, readStream) + }, + + deleteFile(project_id, file_id, callback) { + logger.log({ project_id, file_id }, 'telling file store to delete file') + const opts = { + method: 'delete', + uri: this._buildUrl(project_id, file_id), + timeout: fiveMinsInMs + } + return request(opts, function(err, response) { + if (err != null) { + logger.err( + { err, project_id, file_id }, + 'something went wrong deleting file from filestore' + ) + } + return callback(err) + }) + }, + + copyFile(oldProject_id, oldFile_id, newProject_id, newFile_id, callback) { + logger.log( + { oldProject_id, oldFile_id, newProject_id, newFile_id }, + 'telling filestore to copy a file' + ) + const opts = { + method: 'put', + json: { + source: { + project_id: oldProject_id, + file_id: oldFile_id + } + }, + uri: this._buildUrl(newProject_id, newFile_id), + timeout: fiveMinsInMs + } + return request(opts, function(err, response) { + if (err != null) { + logger.err( + { err, oldProject_id, oldFile_id, newProject_id, newFile_id }, + 'something went wrong telling filestore api to copy file' + ) + return callback(err) + } else if (response.statusCode >= 200 && response.statusCode < 300) { + // successful response + return callback(null, opts.uri) + } else { + err = new Error( + `non-ok response from filestore for copyFile: ${response.statusCode}` + ) + logger.err( + { uri: opts.uri, statusCode: response.statusCode }, + 'error uploading to filestore' + ) + return callback(err) + } + }) + }, + + _buildUrl(project_id, file_id) { + return `${ + settings.apis.filestore.url + }/project/${project_id}/file/${file_id}` + } +} diff --git a/services/web/app/src/Features/HealthCheck/HealthCheckController.js b/services/web/app/src/Features/HealthCheck/HealthCheckController.js new file mode 100644 index 0000000000..c11f3806d8 --- /dev/null +++ b/services/web/app/src/Features/HealthCheck/HealthCheckController.js @@ -0,0 +1,128 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-path-concat, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HealthCheckController +const Mocha = require('mocha') +const Base = require('mocha/lib/reporters/base') +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('health_check') +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const domain = require('domain') +const UserGetter = require('../User/UserGetter') + +module.exports = HealthCheckController = { + check(req, res, next) { + if (next == null) { + next = function(error) {} + } + const d = domain.create() + d.on('error', error => logger.err({ err: error }, 'error in mocha')) + return d.run(function() { + const mocha = new Mocha({ reporter: Reporter(res), timeout: 10000 }) + mocha.addFile('test/smoke/src/SmokeTests.js') + return mocha.run(function() { + // TODO: combine this with the smoke-test-sharelatex module + // we need to clean up all references to the smokeTest module + // so it can be garbage collected. The only reference should + // be in its parent, when it is loaded by mocha.addFile. + const path = require.resolve( + __dirname + '/../../../../test/smoke/src/SmokeTests.js' + ) + const smokeTestModule = require.cache[path] + if (smokeTestModule != null) { + let idx + const { parent } = smokeTestModule + while ((idx = parent.children.indexOf(smokeTestModule)) !== -1) { + parent.children.splice(idx, 1) + } + } else { + logger.warn({ path }, 'smokeTestModule not defined') + } + // remove the smokeTest from the module cache + return delete require.cache[path] + }) + }) + }, + + checkRedis(req, res, next) { + return rclient.healthCheck(function(error) { + if (error != null) { + logger.err({ err: error }, 'failed redis health check') + return res.sendStatus(500) + } else { + return res.sendStatus(200) + } + }) + }, + + checkMongo(req, res, next) { + logger.log('running mongo health check') + return UserGetter.getUserEmail(settings.smokeTest.userId, function( + err, + email + ) { + if (err != null) { + logger.err({ err }, 'mongo health check failed, error present') + return res.sendStatus(500) + } else if (email == null) { + logger.err( + { err }, + 'mongo health check failed, no emai present in find result' + ) + return res.sendStatus(500) + } else { + logger.log({ email }, 'mongo health check passed') + return res.sendStatus(200) + } + }) + } +} + +var Reporter = res => + function(runner) { + Base.call(this, runner) + + const tests = [] + const passes = [] + const failures = [] + + runner.on('test end', test => tests.push(test)) + runner.on('pass', test => passes.push(test)) + runner.on('fail', test => failures.push(test)) + + return runner.on('end', () => { + const clean = test => ({ + title: test.fullTitle(), + duration: test.duration, + err: test.err, + timedOut: test.timedOut + }) + + const results = { + stats: this.stats, + failures: failures.map(clean), + passes: passes.map(clean) + } + + res.contentType('application/json') + if (failures.length > 0) { + logger.err({ failures }, 'health check failed') + return res.status(500).send(JSON.stringify(results, null, 2)) + } else { + return res.status(200).send(JSON.stringify(results, null, 2)) + } + }) + } diff --git a/services/web/app/src/Features/Helpers/EmailHelper.js b/services/web/app/src/Features/Helpers/EmailHelper.js new file mode 100644 index 0000000000..ca8888ce65 --- /dev/null +++ b/services/web/app/src/Features/Helpers/EmailHelper.js @@ -0,0 +1,34 @@ +/* eslint-disable + max-len, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let EmailHelper +const EMAIL_REGEXP = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + +module.exports = EmailHelper = { + parseEmail(email) { + if (email == null) { + return null + } + if (email.length > 254) { + return null + } + email = email.trim().toLowerCase() + + const matched = email.match(EMAIL_REGEXP) + if (matched == null || matched[0] == null) { + return null + } + + return matched[0] + } +} diff --git a/services/web/app/src/Features/Helpers/StringHelper.js b/services/web/app/src/Features/Helpers/StringHelper.js new file mode 100644 index 0000000000..48f308fd28 --- /dev/null +++ b/services/web/app/src/Features/Helpers/StringHelper.js @@ -0,0 +1,30 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +let StringHelper +const JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/g + +const JSON_ESCAPE = { + '&': '\\u0026', + '>': '\\u003e', + '<': '\\u003c', + '\u2028': '\\u2028', + '\u2029': '\\u2029' +} + +module.exports = StringHelper = { + // stringifies and escapes a json object for use in a script. This ensures that &, < and > characters are escaped, + // along with quotes. This ensures that the string can be safely rendered into HTML. See rationale at: + // https://api.rubyonrails.org/classes/ERB/Util.html#method-c-json_escape + // and implementation lifted from: + // https://github.com/ember-fastboot/fastboot/blob/cafd96c48564d8384eb83dc908303dba8ece10fd/src/ember-app.js#L496-L510 + stringifyJsonForScript(object) { + return JSON.stringify(object).replace( + JSON_ESCAPE_REGEXP, + match => JSON_ESCAPE[match] + ) + } +} diff --git a/services/web/app/src/Features/Helpers/UrlHelper.js b/services/web/app/src/Features/Helpers/UrlHelper.js new file mode 100644 index 0000000000..c8d450a605 --- /dev/null +++ b/services/web/app/src/Features/Helpers/UrlHelper.js @@ -0,0 +1,41 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UrlHelper +const Settings = require('settings-sharelatex') + +module.exports = UrlHelper = { + wrapUrlWithProxy(url) { + // TODO: Consider what to do for Community and Enterprise edition? + if ( + __guard__( + Settings.apis != null ? Settings.apis.linkedUrlProxy : undefined, + x => x.url + ) == null + ) { + throw new Error('no linked url proxy configured') + } + return `${Settings.apis.linkedUrlProxy.url}?url=${encodeURIComponent(url)}` + }, + + prependHttpIfNeeded(url) { + if (!url.match('://')) { + url = `http://${url}` + } + return url + } +} +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/History/HistoryController.js b/services/web/app/src/Features/History/HistoryController.js new file mode 100644 index 0000000000..f0d56de343 --- /dev/null +++ b/services/web/app/src/Features/History/HistoryController.js @@ -0,0 +1,390 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HistoryController +const _ = require('lodash') +const async = require('async') +const logger = require('logger-sharelatex') +const request = require('request') +const settings = require('settings-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') +const Errors = require('../Errors/Errors') +const HistoryManager = require('./HistoryManager') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const RestoreManager = require('./RestoreManager') + +module.exports = HistoryController = { + selectHistoryApi(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params != null ? req.params.Project_id : undefined + // find out which type of history service this project uses + return ProjectDetailsHandler.getDetails(project_id, function(err, project) { + if (err != null) { + return next(err) + } + const history = + project.overleaf != null ? project.overleaf.history : undefined + if ( + (history != null ? history.id : undefined) != null && + (history != null ? history.display : undefined) + ) { + req.useProjectHistory = true + } else { + req.useProjectHistory = false + } + return next() + }) + }, + + ensureProjectHistoryEnabled(req, res, next) { + if (next == null) { + next = function(error) {} + } + if (req.useProjectHistory != null) { + return next() + } else { + logger.log({ project_id }, 'project history not enabled') + return res.sendStatus(404) + } + }, + + proxyToHistoryApi(req, res, next) { + if (next == null) { + next = function(error) {} + } + const user_id = AuthenticationController.getLoggedInUserId(req) + const url = + HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url + + logger.log({ url }, 'proxying to history api') + const getReq = request({ + url, + method: req.method, + headers: { + 'X-User-Id': user_id + } + }) + getReq.pipe(res) + return getReq.on('error', function(error) { + logger.error({ url, err: error }, 'history API error') + return next(error) + }) + }, + + proxyToHistoryApiAndInjectUserDetails(req, res, next) { + if (next == null) { + next = function(error) {} + } + const user_id = AuthenticationController.getLoggedInUserId(req) + const url = + HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url + logger.log({ url }, 'proxying to history api') + return HistoryController._makeRequest( + { + url, + method: req.method, + json: true, + headers: { + 'X-User-Id': user_id + } + }, + function(error, body) { + if (error != null) { + return next(error) + } + return HistoryManager.injectUserDetails(body, function(error, data) { + if (error != null) { + return next(error) + } + return res.json(data) + }) + } + ) + }, + + buildHistoryServiceUrl(useProjectHistory) { + // choose a history service, either document-level (trackchanges) + // or project-level (project_history) + if (useProjectHistory) { + return settings.apis.project_history.url + } else { + return settings.apis.trackchanges.url + } + }, + + resyncProjectHistory(req, res, next) { + if (next == null) { + next = function(error) {} + } + const project_id = req.params.Project_id + return ProjectEntityUpdateHandler.resyncProjectHistory(project_id, function( + error + ) { + if (error instanceof Errors.ProjectHistoryDisabledError) { + return res.sendStatus(404) + } + if (error != null) { + return next(error) + } + return res.sendStatus(204) + }) + }, + + restoreFileFromV2(req, res, next) { + const { project_id } = req.params + const { version, pathname } = req.body + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ project_id, version, pathname }, 'restoring file from v2') + return RestoreManager.restoreFileFromV2( + user_id, + project_id, + version, + pathname, + function(error, entity) { + if (error != null) { + return next(error) + } + return res.json({ + type: entity.type, + id: entity._id + }) + } + ) + }, + + restoreDocFromDeletedDoc(req, res, next) { + const { project_id, doc_id } = req.params + const { name } = req.body + const user_id = AuthenticationController.getLoggedInUserId(req) + if (name == null) { + return res.sendStatus(400) // Malformed request + } + logger.log( + { project_id, doc_id, user_id }, + 'restoring doc from v1 deleted doc' + ) + return RestoreManager.restoreDocFromDeletedDoc( + user_id, + project_id, + doc_id, + name, + (err, doc) => { + if (typeof error !== 'undefined' && error !== null) { + return next(error) + } + return res.json({ + doc_id: doc._id + }) + } + ) + }, + + getLabels(req, res, next) { + const project_id = req.params.Project_id + const user_id = AuthenticationController.getLoggedInUserId(req) + return HistoryController._makeRequest( + { + method: 'GET', + url: `${ + settings.apis.project_history.url + }/project/${project_id}/labels`, + json: true + }, + function(error, labels) { + if (error != null) { + return next(error) + } + return res.json(labels) + } + ) + }, + + createLabel(req, res, next) { + const project_id = req.params.Project_id + const { comment, version } = req.body + const user_id = AuthenticationController.getLoggedInUserId(req) + return HistoryController._makeRequest( + { + method: 'POST', + url: `${ + settings.apis.project_history.url + }/project/${project_id}/user/${user_id}/labels`, + json: { comment, version } + }, + function(error, label) { + if (error != null) { + return next(error) + } + return res.json(label) + } + ) + }, + + deleteLabel(req, res, next) { + const project_id = req.params.Project_id + const { label_id } = req.params + const user_id = AuthenticationController.getLoggedInUserId(req) + return HistoryController._makeRequest( + { + method: 'DELETE', + url: `${ + settings.apis.project_history.url + }/project/${project_id}/user/${user_id}/labels/${label_id}` + }, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + }, + + _makeRequest(options, callback) { + return request(options, function(error, response, body) { + if (error != null) { + return callback(error) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + return callback(null, body) + } else { + error = new Error( + `history api responded with non-success code: ${response.statusCode}` + ) + logger.error( + { err: error }, + `project-history api responded with non-success code: ${ + response.statusCode + }` + ) + return callback(error) + } + }) + }, + + downloadZipOfVersion(req, res, next) { + const { project_id, version } = req.params + logger.log({ project_id, version }, 'got request for zip file at version') + return ProjectDetailsHandler.getDetails(project_id, function(err, project) { + if (err != null) { + return next(err) + } + const v1_id = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ) + if (v1_id == null) { + logger.err( + { project_id, version }, + 'got request for zip version of non-v1 history project' + ) + return res.sendStatus(402) + } + return HistoryController._pipeHistoryZipToResponse( + v1_id, + version, + `${project.name} (Version ${version})`, + res, + next + ) + }) + }, + + _pipeHistoryZipToResponse(v1_project_id, version, name, res, next) { + // increase timeout to 6 minutes + res.setTimeout(6 * 60 * 1000) + const url = `${ + settings.apis.v1_history.url + }/projects/${v1_project_id}/version/${version}/zip` + logger.log( + { v1_project_id, version, url }, + 'getting s3 url from history api' + ) + const options = { + auth: { + user: settings.apis.v1_history.user, + pass: settings.apis.v1_history.pass + }, + json: true, + method: 'post', + url + } + return request(options, function(err, response, body) { + if (err) { + logger.error({ err, v1_project_id, version }, 'history API error') + return next(err) + } + let retryAttempt = 0 + let retryDelay = 2000 + // retry for about 6 minutes starting with short delay + return async.retry( + 40, + callback => + setTimeout(function() { + // increase delay by 1 second up to 10 + if (retryDelay < 10000) { + retryDelay += 1000 + } + retryAttempt++ + const getReq = request({ + url: body.zipUrl, + sendImmediately: true + }) + getReq.on('response', function(response) { + if (response.statusCode !== 200) { + return callback(new Error('invalid response')) + } + // pipe also proxies the headers, but we want to customize these ones + delete response.headers['content-disposition'] + delete response.headers['content-type'] + res.status(response.statusCode) + res.setContentDisposition('attachment', { + filename: `${name}.zip` + }) + res.contentType('application/zip') + getReq.pipe(res) + return callback() + }) + return getReq.on('error', function(err) { + logger.error( + { err, v1_project_id, version, retryAttempt }, + 'history s3 download error' + ) + return callback(err) + }) + }, retryDelay), + function(err) { + if (err) { + logger.error( + { err, v1_project_id, version, retryAttempt }, + 'history s3 download failed' + ) + return next(err) + } + } + ) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/History/HistoryManager.js b/services/web/app/src/Features/History/HistoryManager.js new file mode 100644 index 0000000000..0ac297e09f --- /dev/null +++ b/services/web/app/src/Features/History/HistoryManager.js @@ -0,0 +1,206 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HistoryManager +const request = require('request') +const settings = require('settings-sharelatex') +const async = require('async') +const UserGetter = require('../User/UserGetter') + +module.exports = HistoryManager = { + initializeProject(callback) { + if (callback == null) { + callback = function(error, history_id) {} + } + if ( + !(settings.apis.project_history != null + ? settings.apis.project_history.initializeHistoryForNewProjects + : undefined) + ) { + return callback() + } + return request.post( + { + url: `${settings.apis.project_history.url}/project` + }, + function(error, res, body) { + if (error != null) { + return callback(error) + } + + if (res.statusCode >= 200 && res.statusCode < 300) { + let project + try { + project = JSON.parse(body) + } catch (error1) { + error = error1 + return callback(error) + } + + const overleaf_id = __guard__( + project != null ? project.project : undefined, + x => x.id + ) + if (!overleaf_id) { + error = new Error('project-history did not provide an id', project) + return callback(error) + } + + return callback(null, { overleaf_id }) + } else { + error = new Error( + `project-history returned a non-success status code: ${ + res.statusCode + }` + ) + return callback(error) + } + } + ) + }, + + flushProject(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return request.post( + { + url: `${settings.apis.project_history.url}/project/${project_id}/flush` + }, + function(error, res, body) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback() + } else { + error = new Error( + `project-history returned a non-success status code: ${ + res.statusCode + }` + ) + return callback(error) + } + } + ) + }, + + resyncProject(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return request.post( + { + url: `${settings.apis.project_history.url}/project/${project_id}/resync` + }, + function(error, res, body) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback() + } else { + error = new Error( + `project-history returned a non-success status code: ${ + res.statusCode + }` + ) + return callback(error) + } + } + ) + }, + + injectUserDetails(data, callback) { + // data can be either: + // { + // diff: [{ + // i: "foo", + // meta: { + // users: ["user_id", { first_name: "James", ... }, ...] + // ... + // } + // }, ...] + // } + // or + // { + // updates: [{ + // pathnames: ["main.tex"] + // meta: { + // users: ["user_id", { first_name: "James", ... }, ...] + // ... + // }, + // ... + // }, ...] + // } + // Either way, the top level key points to an array of objects with a meta.users property + // that we need to replace user_ids with populated user objects. + // Note that some entries in the users arrays may already have user objects from the v1 history + // service + let entry, user + if (callback == null) { + callback = function(error, data_with_users) {} + } + let user_ids = new Set() + for (entry of Array.from(data.diff || data.updates || [])) { + for (user of Array.from( + (entry.meta != null ? entry.meta.users : undefined) || [] + )) { + if (typeof user === 'string') { + user_ids.add(user) + } + } + } + user_ids = Array.from(user_ids) + return UserGetter.getUsers( + user_ids, + { first_name: 1, last_name: 1, email: 1 }, + function(error, users_array) { + if (error != null) { + return callback(error) + } + const users = {} + for (user of Array.from(users_array || [])) { + users[user._id.toString()] = HistoryManager._userView(user) + } + for (entry of Array.from(data.diff || data.updates || [])) { + if (entry.meta != null) { + entry.meta.users = ( + (entry.meta != null ? entry.meta.users : undefined) || [] + ).map(function(user) { + if (typeof user === 'string') { + return users[user] + } else { + return user + } + }) + } + } + return callback(null, data) + } + ) + }, + + _userView(user) { + const { _id, first_name, last_name, email } = user + return { first_name, last_name, email, id: _id } + } +} +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/History/RestoreManager.js b/services/web/app/src/Features/History/RestoreManager.js new file mode 100644 index 0000000000..6bea3cfb32 --- /dev/null +++ b/services/web/app/src/Features/History/RestoreManager.js @@ -0,0 +1,157 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RestoreManager +const Settings = require('settings-sharelatex') +const Path = require('path') +const FileWriter = require('../../infrastructure/FileWriter') +const FileSystemImportManager = require('../Uploads/FileSystemImportManager') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const ProjectLocator = require('../Project/ProjectLocator') +const EditorController = require('../Editor/EditorController') +const Errors = require('../Errors/Errors') +const moment = require('moment') + +module.exports = RestoreManager = { + restoreDocFromDeletedDoc(user_id, project_id, doc_id, name, callback) { + // This is the legacy method for restoring a doc from the SL track-changes/deletedDocs system. + // It looks up the deleted doc's contents, and then creates a new doc with the same content. + // We don't actually remove the deleted doc entry, just create a new one from its lines. + if (callback == null) { + callback = function(error, doc, folder_id) {} + } + return ProjectEntityHandler.getDoc( + project_id, + doc_id, + { include_deleted: true }, + function(error, lines) { + if (error != null) { + return callback(error) + } + const addDocWithName = (name, callback) => + EditorController.addDoc( + project_id, + null, + name, + lines, + 'restore', + user_id, + callback + ) + return RestoreManager._addEntityWithUniqueName( + addDocWithName, + name, + callback + ) + } + ) + }, + + restoreFileFromV2(user_id, project_id, version, pathname, callback) { + if (callback == null) { + callback = function(error, entity) {} + } + return RestoreManager._writeFileVersionToDisk( + project_id, + version, + pathname, + function(error, fsPath) { + if (error != null) { + return callback(error) + } + const basename = Path.basename(pathname) + let dirname = Path.dirname(pathname) + if (dirname === '.') { + // no directory + dirname = '' + } + return RestoreManager._findOrCreateFolder(project_id, dirname, function( + error, + parent_folder_id + ) { + if (error != null) { + return callback(error) + } + const addEntityWithName = (name, callback) => + FileSystemImportManager.addEntity( + user_id, + project_id, + parent_folder_id, + name, + fsPath, + false, + callback + ) + return RestoreManager._addEntityWithUniqueName( + addEntityWithName, + basename, + callback + ) + }) + } + ) + }, + + _findOrCreateFolder(project_id, dirname, callback) { + if (callback == null) { + callback = function(error, folder_id) {} + } + return EditorController.mkdirp(project_id, dirname, function( + error, + newFolders, + lastFolder + ) { + if (error != null) { + return callback(error) + } + return callback(null, lastFolder != null ? lastFolder._id : undefined) + }) + }, + + _addEntityWithUniqueName(addEntityWithName, basename, callback) { + if (callback == null) { + callback = function(error) {} + } + return addEntityWithName(basename, function(error, entity) { + if (error != null) { + if (error instanceof Errors.InvalidNameError) { + // likely a duplicate name, so try with a prefix + const date = moment(new Date()).format('Do MMM YY H:mm:ss') + // Move extension to the end so the file type is preserved + const extension = Path.extname(basename) + basename = Path.basename(basename, extension) + basename = `${basename} (Restored on ${date})` + if (extension !== '') { + basename = `${basename}${extension}` + } + return addEntityWithName(basename, callback) + } else { + return callback(error) + } + } else { + return callback(null, entity) + } + }) + }, + + _writeFileVersionToDisk(project_id, version, pathname, callback) { + if (callback == null) { + callback = function(error, fsPath) {} + } + const url = `${ + Settings.apis.project_history.url + }/project/${project_id}/version/${version}/${encodeURIComponent(pathname)}` + return FileWriter.writeUrlToDisk(project_id, url, callback) + } +} diff --git a/services/web/app/src/Features/InactiveData/InactiveProjectController.js b/services/web/app/src/Features/InactiveData/InactiveProjectController.js new file mode 100644 index 0000000000..0d8134714d --- /dev/null +++ b/services/web/app/src/Features/InactiveData/InactiveProjectController.js @@ -0,0 +1,48 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const InactiveProjectManager = require('./InactiveProjectManager') +const logger = require('logger-sharelatex') + +module.exports = { + deactivateOldProjects(req, res) { + logger.log('recived request to deactivate old projects') + const numberOfProjectsToArchive = parseInt( + req.body.numberOfProjectsToArchive, + 10 + ) + const { ageOfProjects } = req.body + return InactiveProjectManager.deactivateOldProjects( + numberOfProjectsToArchive, + ageOfProjects, + function(err, projectsDeactivated) { + if (err != null) { + return res.sendStatus(500) + } else { + return res.send(projectsDeactivated) + } + } + ) + }, + + deactivateProject(req, res) { + const { project_id } = req.params + logger.log({ project_id }, 'recived request to deactivating project') + return InactiveProjectManager.deactivateProject(project_id, function(err) { + if (err != null) { + return res.sendStatus(500) + } else { + return res.sendStatus(200) + } + }) + } +} diff --git a/services/web/app/src/Features/InactiveData/InactiveProjectManager.js b/services/web/app/src/Features/InactiveData/InactiveProjectManager.js new file mode 100644 index 0000000000..14ab230422 --- /dev/null +++ b/services/web/app/src/Features/InactiveData/InactiveProjectManager.js @@ -0,0 +1,107 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let InactiveProjectManager +const async = require('async') +const _ = require('underscore') +const logger = require('logger-sharelatex') +const DocstoreManager = require('../Docstore/DocstoreManager') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectUpdateHandler = require('../Project/ProjectUpdateHandler') +const { Project } = require('../../models/Project') + +const MILISECONDS_IN_DAY = 86400000 +module.exports = InactiveProjectManager = { + reactivateProjectIfRequired(project_id, callback) { + return ProjectGetter.getProject(project_id, { active: true }, function( + err, + project + ) { + if (err != null) { + logger.err({ err, project_id }, 'error getting project') + return callback(err) + } + logger.log( + { project_id, active: project.active }, + 'seeing if need to reactivate project' + ) + + if (project.active) { + return callback() + } + + return DocstoreManager.unarchiveProject(project_id, function(err) { + if (err != null) { + logger.err( + { err, project_id }, + 'error reactivating project in docstore' + ) + return callback(err) + } + return ProjectUpdateHandler.markAsActive(project_id, callback) + }) + }) + }, + + deactivateOldProjects(limit, daysOld, callback) { + if (limit == null) { + limit = 10 + } + if (daysOld == null) { + daysOld = 360 + } + const oldProjectDate = new Date() - MILISECONDS_IN_DAY * daysOld + logger.log( + { oldProjectDate, limit, daysOld }, + 'starting process of deactivating old projects' + ) + return Project.find() + .where('lastOpened') + .lt(oldProjectDate) + .where('active') + .equals(true) + .select('_id') + .limit(limit) + .exec(function(err, projects) { + if (err != null) { + logger.err({ err }, 'could not get projects for deactivating') + } + const jobs = _.map(projects, project => cb => + InactiveProjectManager.deactivateProject(project._id, cb) + ) + logger.log( + { numberOfProjects: projects != null ? projects.length : undefined }, + 'deactivating projects' + ) + return async.series(jobs, function(err) { + if (err != null) { + logger.err({ err }, 'error deactivating projects') + } + return callback(err, projects) + }) + }) + }, + + deactivateProject(project_id, callback) { + logger.log({ project_id }, 'deactivating inactive project') + const jobs = [ + cb => DocstoreManager.archiveProject(project_id, cb), + cb => ProjectUpdateHandler.markAsInactive(project_id, cb) + ] + return async.series(jobs, function(err) { + if (err != null) { + logger.err({ err, project_id }, 'error deactivating project') + } + return callback(err) + }) + } +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsAPI.js b/services/web/app/src/Features/Institutions/InstitutionsAPI.js new file mode 100644 index 0000000000..0a2d7c9447 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsAPI.js @@ -0,0 +1,222 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let InstitutionsAPI +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const settings = require('settings-sharelatex') +const request = require('request') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') + +module.exports = InstitutionsAPI = { + getInstitutionAffiliations(institutionId, callback) { + if (callback == null) { + callback = function(error, body) {} + } + return makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't get institution affiliations" + }, + (error, body) => callback(error, body || []) + ) + }, + + getInstitutionLicences(institutionId, startDate, endDate, lag, callback) { + if (callback == null) { + callback = function(error, body) {} + } + return makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/institution_licences`, + body: { start_date: startDate, end_date: endDate, lag }, + defaultErrorMessage: "Couldn't get institution licences" + }, + callback + ) + }, + + getUserAffiliations(userId, callback) { + if (callback == null) { + callback = function(error, body) {} + } + return makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/users/${userId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't get user affiliations" + }, + (error, body) => callback(error, body || []) + ) + }, + + addAffiliation(userId, email, affiliationOptions, callback) { + if (callback == null) { + // affiliationOptions is optional + callback = affiliationOptions + affiliationOptions = {} + } + + const { university, department, role, confirmedAt } = affiliationOptions + return makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId.toString()}/affiliations`, + body: { email, university, department, role, confirmedAt }, + defaultErrorMessage: "Couldn't create affiliation" + }, + function(error, body) { + if (error) { + return callback(error, body) + } + // have notifications delete any ip matcher notifications for this university + logger.log(university) + return NotificationsBuilder.ipMatcherAffiliation(userId).read( + university != null ? university.id : undefined, + function(err) { + if (err) { + logger.err( + { err }, + 'Something went wrong marking ip notifications read' + ) + } + return callback(error, body) + } + ) + } + ) + }, + + removeAffiliation(userId, email, callback) { + if (callback == null) { + callback = function(error) {} + } + return makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId.toString()}/affiliations/remove`, + body: { email }, + extraSuccessStatusCodes: [404], // `Not Found` responses are considered successful + defaultErrorMessage: "Couldn't remove affiliation" + }, + callback + ) + }, + + endorseAffiliation(userId, email, role, department, callback) { + if (callback == null) { + callback = function(error) {} + } + return makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId.toString()}/affiliations/endorse`, + body: { email, role, department }, + defaultErrorMessage: "Couldn't endorse affiliation" + }, + callback + ) + }, + + deleteAffiliations(userId, callback) { + if (callback == null) { + callback = function(error) {} + } + return makeAffiliationRequest( + { + method: 'DELETE', + path: `/api/v2/users/${userId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't delete affiliations" + }, + callback + ) + } +} + +var makeAffiliationRequest = function(requestOptions, callback) { + if (callback == null) { + callback = function(error) {} + } + if ( + !__guard__( + __guard__(settings != null ? settings.apis : undefined, x1 => x1.v1), + x => x.url + ) + ) { + return callback(null) + } // service is not configured + if (!requestOptions.extraSuccessStatusCodes) { + requestOptions.extraSuccessStatusCodes = [] + } + return request( + { + method: requestOptions.method, + url: `${settings.apis.v1.url}${requestOptions.path}`, + body: requestOptions.body, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + json: true, + timeout: 20 * 1000 + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + let isSuccess = response.statusCode >= 200 && response.statusCode < 300 + if (!isSuccess) { + isSuccess = Array.from(requestOptions.extraSuccessStatusCodes).includes( + response.statusCode + ) + } + if (!isSuccess) { + let errorMessage + if (body != null ? body.errors : undefined) { + errorMessage = `${response.statusCode}: ${body.errors}` + } else { + errorMessage = `${requestOptions.defaultErrorMessage}: ${ + response.statusCode + }` + } + + logger.err( + { path: requestOptions.path, body: requestOptions.body }, + errorMessage + ) + return callback(new Error(errorMessage)) + } + + return callback(null, body) + } + ) +} +;[ + 'getInstitutionAffiliations', + 'getUserAffiliations', + 'addAffiliation', + 'removeAffiliation' +].map(method => + metrics.timeAsyncMethod( + InstitutionsAPI, + method, + 'mongo.InstitutionsAPI', + logger + ) +) + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsController.js b/services/web/app/src/Features/Institutions/InstitutionsController.js new file mode 100644 index 0000000000..6621e0af19 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsController.js @@ -0,0 +1,93 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let InstitutionsController +const logger = require('logger-sharelatex') +const UserGetter = require('../User/UserGetter') +const { addAffiliation } = require('../Institutions/InstitutionsAPI') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') +const async = require('async') + +module.exports = InstitutionsController = { + confirmDomain(req, res, next) { + const { hostname } = req.body + return affiliateUsers(hostname, function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + } +} + +var affiliateUsers = function(hostname, callback) { + if (callback == null) { + callback = function(error) {} + } + const reversedHostname = hostname + .trim() + .split('') + .reverse() + .join('') + return UserGetter.getUsersByHostname( + hostname, + { _id: 1, emails: 1 }, + function(error, users) { + if (error != null) { + logger.err({ error }, 'problem fetching users by hostname') + return callback(error) + } + + return async.map( + users, + (user, innerCallback) => + affiliateUserByReversedHostname( + user, + reversedHostname, + innerCallback + ), + callback + ) + } + ) +} + +var affiliateUserByReversedHostname = function( + user, + reversedHostname, + callback +) { + const matchingEmails = user.emails.filter( + email => email.reversedHostname === reversedHostname + ) + return async.map( + matchingEmails, + (email, innerCallback) => + addAffiliation( + user._id, + email.email, + { confirmedAt: email.confirmedAt }, + error => { + if (error != null) { + logger.err( + { error }, + 'problem adding affiliation while confirming hostname' + ) + return innerCallback(error) + } + return FeaturesUpdater.refreshFeatures(user._id, true, innerCallback) + } + ), + callback + ) +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsFeatures.js b/services/web/app/src/Features/Institutions/InstitutionsFeatures.js new file mode 100644 index 0000000000..cec54a5c29 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsFeatures.js @@ -0,0 +1,71 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let InstitutionsFeatures +const InstitutionsGetter = require('./InstitutionsGetter') +const PlansLocator = require('../Subscription/PlansLocator') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') + +module.exports = InstitutionsFeatures = { + getInstitutionsFeatures(userId, callback) { + if (callback == null) { + callback = function(error, features) {} + } + return InstitutionsFeatures.getInstitutionsPlan(userId, function( + error, + plan + ) { + if (error != null) { + return callback(error) + } + plan = PlansLocator.findLocalPlanInSettings(plan) + return callback(null, (plan != null ? plan.features : undefined) || {}) + }) + }, + + getInstitutionsPlan(userId, callback) { + if (callback == null) { + callback = function(error, plan) {} + } + return InstitutionsFeatures.hasLicence(userId, function(error, hasLicence) { + if (error != null) { + return callback(error) + } + if (!hasLicence) { + return callback(null, null) + } + return callback(null, Settings.institutionPlanCode) + }) + }, + + hasLicence(userId, callback) { + if (callback == null) { + callback = function(error, hasLicence) {} + } + return InstitutionsGetter.getConfirmedInstitutions(userId, function( + error, + institutions + ) { + if (error != null) { + return callback(error) + } + + const hasLicence = institutions.some( + institution => institution.licence && institution.licence !== 'free' + ) + + return callback(null, hasLicence) + }) + } +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsGetter.js b/services/web/app/src/Features/Institutions/InstitutionsGetter.js new file mode 100644 index 0000000000..09e08e5b42 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsGetter.js @@ -0,0 +1,70 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let InstitutionsGetter +const UserGetter = require('../User/UserGetter') +const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler') +const UserMembershipEntityConfigs = require('../UserMembership/UserMembershipEntityConfigs') +const logger = require('logger-sharelatex') + +module.exports = InstitutionsGetter = { + getConfirmedInstitutions(userId, callback) { + if (callback == null) { + callback = function(error, institutions) {} + } + return UserGetter.getUserFullEmails(userId, function(error, emailsData) { + if (error != null) { + return callback(error) + } + + const confirmedInstitutions = emailsData + .filter( + emailData => + emailData.confirmedAt != null && + __guard__( + emailData.affiliation != null + ? emailData.affiliation.institution + : undefined, + x => x.confirmed + ) + ) + .map( + emailData => + emailData.affiliation != null + ? emailData.affiliation.institution + : undefined + ) + + return callback(null, confirmedInstitutions) + }) + }, + + getManagedInstitutions(user_id, callback) { + if (callback == null) { + callback = function(error, managedInstitutions) {} + } + return UserMembershipsHandler.getEntitiesByUser( + UserMembershipEntityConfigs.institution, + user_id, + callback + ) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsManager.js b/services/web/app/src/Features/Institutions/InstitutionsManager.js new file mode 100644 index 0000000000..b2597cca5e --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsManager.js @@ -0,0 +1,190 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let InstitutionsManager +const logger = require('logger-sharelatex') +const async = require('async') +const { db } = require('../../infrastructure/mongojs') +const _ = require('underscore') +const { ObjectId } = require('../../infrastructure/mongojs') +const { getInstitutionAffiliations } = require('./InstitutionsAPI') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') +const UserGetter = require('../User/UserGetter') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') +const { Institution } = require('../../models/Institution') +const { Subscription } = require('../../models/Subscription') + +const ASYNC_LIMIT = 10 +module.exports = InstitutionsManager = { + upgradeInstitutionUsers(institutionId, callback) { + if (callback == null) { + callback = function(error) {} + } + return async.waterfall( + [ + cb => fetchInstitutionAndAffiliations(institutionId, cb), + function(institution, affiliations, cb) { + affiliations = _.map(affiliations, function(affiliation) { + affiliation.institutionName = institution.name + affiliation.institutionId = institutionId + return affiliation + }) + return async.eachLimit( + affiliations, + ASYNC_LIMIT, + refreshFeatures, + err => cb(err) + ) + } + ], + callback + ) + }, + + checkInstitutionUsers(institutionId, callback) { + if (callback == null) { + callback = function(error) {} + } + return getInstitutionAffiliations(institutionId, (error, affiliations) => + UserGetter.getUsersByAnyConfirmedEmail( + affiliations.map(affiliation => affiliation.email), + { features: 1 }, + (error, users) => callback(error, checkFeatures(users)) + ) + ) + }, + + getInstitutionUsersSubscriptions(institutionId, callback) { + if (callback == null) { + callback = function(error, subscriptions) {} + } + return getInstitutionAffiliations(institutionId, function( + error, + affiliations + ) { + if (error != null) { + return callback(error) + } + const userIds = affiliations.map(affiliation => + ObjectId(affiliation.user_id) + ) + return Subscription.find({ + admin_id: userIds, + planCode: { $not: /trial/ } + }) + .populate('admin_id', 'email') + .exec(callback) + }) + } +} + +var fetchInstitutionAndAffiliations = (institutionId, callback) => + async.waterfall( + [ + cb => + Institution.findOne({ v1Id: institutionId }, (err, institution) => + cb(err, institution) + ), + (institution, cb) => + institution.fetchV1Data((err, institution) => cb(err, institution)), + (institution, cb) => + getInstitutionAffiliations(institutionId, (err, affiliations) => + cb(err, institution, affiliations) + ) + ], + callback + ) + +var refreshFeatures = function(affiliation, callback) { + const userId = ObjectId(affiliation.user_id) + return async.waterfall( + [ + cb => + FeaturesUpdater.refreshFeatures( + userId, + true, + (err, features, featuresChanged) => cb(err, featuresChanged) + ), + (featuresChanged, cb) => + getUserInfo(userId, (error, user, subscription) => + cb(error, user, subscription, featuresChanged) + ), + (user, subscription, featuresChanged, cb) => + notifyUser(user, affiliation, subscription, featuresChanged, cb) + ], + callback + ) +} + +var getUserInfo = (userId, callback) => + async.waterfall( + [ + cb => UserGetter.getUser(userId, cb), + (user, cb) => + SubscriptionLocator.getUsersSubscription(user, (err, subscription) => + cb(err, user, subscription) + ) + ], + callback + ) + +var notifyUser = (user, affiliation, subscription, featuresChanged, callback) => + async.parallel( + [ + function(cb) { + if (featuresChanged) { + return NotificationsBuilder.featuresUpgradedByAffiliation( + affiliation, + user + ).create(cb) + } else { + return cb() + } + }, + function(cb) { + if ( + subscription != null && + subscription.planCode.match(/(free|trial)/) == null && + !subscription.groupPlan + ) { + return NotificationsBuilder.redundantPersonalSubscription( + affiliation, + user + ).create(cb) + } else { + return cb() + } + } + ], + callback + ) + +var checkFeatures = function(users) { + const usersSummary = { + totalConfirmedUsers: users.length, + totalConfirmedProUsers: 0, + totalConfirmedNonProUsers: 0, + confirmedNonProUsers: [] + } + users.forEach(function(user) { + if (user.features.collaborators === -1 && user.features.trackChanges) { + return (usersSummary.totalConfirmedProUsers += 1) + } else { + usersSummary.totalConfirmedNonProUsers += 1 + return usersSummary.confirmedNonProUsers.push(user._id) + } + }) + return usersSummary +} diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js new file mode 100644 index 0000000000..78bbf2b896 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js @@ -0,0 +1,186 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LinkedFilesController +const AuthenticationController = require('../Authentication/AuthenticationController') +const EditorController = require('../Editor/EditorController') +const ProjectLocator = require('../Project/ProjectLocator') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const { + UrlFetchFailedError, + InvalidUrlError, + OutputFileFetchFailedError, + AccessDeniedError, + BadEntityTypeError, + BadDataError, + ProjectNotFoundError, + V1ProjectNotFoundError, + SourceFileNotFoundError, + NotOriginalImporterError, + FeatureNotAvailableError, + RemoteServiceError, + FileCannotRefreshError +} = require('./LinkedFilesErrors') +const Modules = require('../../infrastructure/Modules') + +module.exports = LinkedFilesController = { + Agents: _.extend( + { + url: require('./UrlAgent'), + project_file: require('./ProjectFileAgent'), + project_output_file: require('./ProjectOutputFileAgent') + }, + Modules.linkedFileAgentsIncludes() + ), + + _getAgent(provider) { + if (!LinkedFilesController.Agents.hasOwnProperty(provider)) { + return null + } + if (!Array.from(Settings.enabledLinkedFileTypes).includes(provider)) { + return null + } + return LinkedFilesController.Agents[provider] + }, + + createLinkedFile(req, res, next) { + const { project_id } = req.params + const { name, provider, data, parent_folder_id } = req.body + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log( + { project_id, name, provider, data, parent_folder_id, user_id }, + 'create linked file request' + ) + + const Agent = LinkedFilesController._getAgent(provider) + if (Agent == null) { + return res.sendStatus(400) + } + + data.provider = provider + + return Agent.createLinkedFile( + project_id, + data, + name, + parent_folder_id, + user_id, + function(err, newFileId) { + if (err != null) { + return LinkedFilesController.handleError(err, req, res, next) + } + return res.json({ new_file_id: newFileId }) + } + ) + }, + + refreshLinkedFile(req, res, next) { + const { project_id, file_id } = req.params + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ project_id, file_id, user_id }, 'refresh linked file request') + + return LinkedFilesHandler.getFileById(project_id, file_id, function( + err, + file, + path, + parentFolder + ) { + if (err != null) { + return next(err) + } + if (file == null) { + return res.sendStatus(404) + } + const { name } = file + const { linkedFileData } = file + if ( + linkedFileData == null || + (linkedFileData != null ? linkedFileData.provider : undefined) == null + ) { + return res.send(409) + } + const { provider } = linkedFileData + const parent_folder_id = parentFolder._id + const Agent = LinkedFilesController._getAgent(provider) + if (Agent == null) { + return res.sendStatus(400) + } + + return Agent.refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + function(err, newFileId) { + if (err != null) { + return LinkedFilesController.handleError(err, req, res, next) + } + return res.json({ new_file_id: newFileId }) + } + ) + }) + }, + + handleError(error, req, res, next) { + if (error instanceof BadDataError) { + return res.status(400).send('The submitted data is not valid') + } else if (error instanceof AccessDeniedError) { + return res.status(403).send('You do not have access to this project') + } else if (error instanceof BadDataError) { + return res.status(400).send('The submitted data is not valid') + } else if (error instanceof BadEntityTypeError) { + return res.status(400).send('The file is the wrong type') + } else if (error instanceof SourceFileNotFoundError) { + return res.status(404).send('Source file not found') + } else if (error instanceof ProjectNotFoundError) { + return res.status(404).send('Project not found') + } else if (error instanceof V1ProjectNotFoundError) { + return res + .status(409) + .send( + 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file' + ) + } else if (error instanceof OutputFileFetchFailedError) { + return res.status(404).send('Could not get output file') + } else if (error instanceof UrlFetchFailedError) { + return res + .status(422) + .send( + `Your URL could not be reached (${ + error.statusCode + } status code). Please check it and try again.` + ) + } else if (error instanceof InvalidUrlError) { + return res + .status(422) + .send('Your URL is not valid. Please check it and try again.') + } else if (error instanceof NotOriginalImporterError) { + return res + .status(400) + .send('You are not the user who originally imported this file') + } else if (error instanceof FeatureNotAvailableError) { + return res.status(400).send('This feature is not enabled on your account') + } else if (error instanceof RemoteServiceError) { + return res.status(502).send('The remote service produced an error') + } else if (error instanceof FileCannotRefreshError) { + return res.status(400).send('This file cannot be refreshed') + } else { + return next(error) + } + } +} diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesErrors.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesErrors.js new file mode 100644 index 0000000000..7a10066842 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesErrors.js @@ -0,0 +1,124 @@ +/* eslint-disable + no-proto, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +var UrlFetchFailedError = function(message) { + const error = new Error(message) + error.name = 'UrlFetchFailedError' + error.__proto__ = UrlFetchFailedError.prototype + return error +} +UrlFetchFailedError.prototype.__proto__ = Error.prototype + +var InvalidUrlError = function(message) { + const error = new Error(message) + error.name = 'InvalidUrlError' + error.__proto__ = InvalidUrlError.prototype + return error +} +InvalidUrlError.prototype.__proto__ = Error.prototype + +var OutputFileFetchFailedError = function(message) { + const error = new Error(message) + error.name = 'OutputFileFetchFailedError' + error.__proto__ = OutputFileFetchFailedError.prototype + return error +} +OutputFileFetchFailedError.prototype.__proto__ = Error.prototype + +var AccessDeniedError = function(message) { + const error = new Error(message) + error.name = 'AccessDenied' + error.__proto__ = AccessDeniedError.prototype + return error +} +AccessDeniedError.prototype.__proto__ = Error.prototype + +var BadEntityTypeError = function(message) { + const error = new Error(message) + error.name = 'BadEntityType' + error.__proto__ = BadEntityTypeError.prototype + return error +} +BadEntityTypeError.prototype.__proto__ = Error.prototype + +var BadDataError = function(message) { + const error = new Error(message) + error.name = 'BadData' + error.__proto__ = BadDataError.prototype + return error +} +BadDataError.prototype.__proto__ = Error.prototype + +var ProjectNotFoundError = function(message) { + const error = new Error(message) + error.name = 'ProjectNotFound' + error.__proto__ = ProjectNotFoundError.prototype + return error +} +ProjectNotFoundError.prototype.__proto__ = Error.prototype + +var V1ProjectNotFoundError = function(message) { + const error = new Error(message) + error.name = 'V1ProjectNotFound' + error.__proto__ = V1ProjectNotFoundError.prototype + return error +} +V1ProjectNotFoundError.prototype.__proto__ = Error.prototype + +var SourceFileNotFoundError = function(message) { + const error = new Error(message) + error.name = 'SourceFileNotFound' + error.__proto__ = SourceFileNotFoundError.prototype + return error +} +SourceFileNotFoundError.prototype.__proto__ = Error.prototype + +var NotOriginalImporterError = function(message) { + const error = new Error(message) + error.name = 'NotOriginalImporter' + error.__proto__ = NotOriginalImporterError.prototype + return error +} +NotOriginalImporterError.prototype.__proto__ = Error.prototype + +var FeatureNotAvailableError = function(message) { + const error = new Error(message) + error.name = 'FeatureNotAvailable' + error.__proto__ = FeatureNotAvailableError.prototype + return error +} +FeatureNotAvailableError.prototype.__proto__ = Error.prototype + +var RemoteServiceError = function(message) { + const error = new Error(message) + error.name = 'RemoteService' + error.__proto__ = RemoteServiceError.prototype + return error +} +RemoteServiceError.prototype.__proto__ = Error.prototype + +var FileCannotRefreshError = function(message) { + const error = new Error(message) + error.name = 'RemoteService' + error.__proto__ = FileCannotRefreshError.prototype + return error +} +FileCannotRefreshError.prototype.__proto__ = Error.prototype + +module.exports = { + UrlFetchFailedError, + InvalidUrlError, + OutputFileFetchFailedError, + AccessDeniedError, + BadEntityTypeError, + BadDataError, + ProjectNotFoundError, + V1ProjectNotFoundError, + SourceFileNotFoundError, + NotOriginalImporterError, + FeatureNotAvailableError, + RemoteServiceError, + FileCannotRefreshError +} diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesHandler.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesHandler.js new file mode 100644 index 0000000000..171076e929 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesHandler.js @@ -0,0 +1,161 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LinkedFilesHandler +const FileWriter = require('../../infrastructure/FileWriter') +const EditorController = require('../Editor/EditorController') +const ProjectLocator = require('../Project/ProjectLocator') +const { Project } = require('../../models/Project') +const ProjectGetter = require('../Project/ProjectGetter') +const _ = require('underscore') +const { + ProjectNotFoundError, + V1ProjectNotFoundError, + BadDataError +} = require('./LinkedFilesErrors') + +module.exports = LinkedFilesHandler = { + getFileById(project_id, file_id, callback) { + if (callback == null) { + callback = function(err, file) {} + } + return ProjectLocator.findElement( + { + project_id, + element_id: file_id, + type: 'file' + }, + function(err, file, path, parentFolder) { + if (err != null) { + return callback(err) + } + return callback(null, file, path, parentFolder) + } + ) + }, + + getSourceProject(data, callback) { + if (callback == null) { + callback = function(err, project) {} + } + const projection = { _id: 1, name: 1 } + if (data.v1_source_doc_id != null) { + return Project.findOne( + { 'overleaf.id': data.v1_source_doc_id }, + projection, + function(err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new V1ProjectNotFoundError()) + } + return callback(null, project) + } + ) + } else if (data.source_project_id != null) { + return ProjectGetter.getProject( + data.source_project_id, + projection, + function(err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new ProjectNotFoundError()) + } + return callback(null, project) + } + ) + } else { + return callback(new BadDataError('neither v1 nor v2 id present')) + } + }, + + importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (callback == null) { + callback = function(err, file) {} + } + callback = _.once(callback) + return FileWriter.writeStreamToDisk(project_id, readStream, function( + err, + fsPath + ) { + if (err != null) { + return callback(err) + } + return EditorController.upsertFile( + project_id, + parent_folder_id, + name, + fsPath, + linkedFileData, + 'upload', + user_id, + (err, file) => { + if (err != null) { + return callback(err) + } + return callback(null, file) + } + ) + }) + }, + + importContent( + project_id, + content, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (callback == null) { + callback = function(err, file) {} + } + callback = _.once(callback) + return FileWriter.writeContentToDisk(project_id, content, function( + err, + fsPath + ) { + if (err != null) { + return callback(err) + } + return EditorController.upsertFile( + project_id, + parent_folder_id, + name, + fsPath, + linkedFileData, + 'upload', + user_id, + (err, file) => { + if (err != null) { + return callback(err) + } + return callback(null, file) + } + ) + }) + } +} diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesRouter.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesRouter.js new file mode 100644 index 0000000000..9e04c8e353 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesRouter.js @@ -0,0 +1,44 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const AuthenticationController = require('../Authentication/AuthenticationController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const LinkedFilesController = require('./LinkedFilesController') + +module.exports = { + apply(webRouter) { + webRouter.post( + '/project/:project_id/linked_file', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-linked-file', + params: ['project_id'], + maxRequests: 100, + timeInterval: 60 + }), + LinkedFilesController.createLinkedFile + ) + + return webRouter.post( + '/project/:project_id/linked_file/:file_id/refresh', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'refresh-linked-file', + params: ['project_id'], + maxRequests: 100, + timeInterval: 60 + }), + LinkedFilesController.refreshLinkedFile + ) + } +} diff --git a/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js b/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js new file mode 100644 index 0000000000..f2642a6e65 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js @@ -0,0 +1,262 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectFileAgent +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const ProjectLocator = require('../Project/ProjectLocator') +const ProjectGetter = require('../Project/ProjectGetter') +const DocstoreManager = require('../Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const FileStoreHandler = require('../FileStore/FileStoreHandler') +const _ = require('underscore') +const Settings = require('settings-sharelatex') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const { + BadDataError, + AccessDeniedError, + BadEntityTypeError, + SourceFileNotFoundError, + ProjectNotFoundError, + V1ProjectNotFoundError +} = require('./LinkedFilesErrors') + +module.exports = ProjectFileAgent = { + createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (!this._canCreate(linkedFileData)) { + return callback(new AccessDeniedError()) + } + return this._go( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) + }, + + refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + return this._go( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) + }, + + _prepare(project_id, linkedFileData, user_id, callback) { + if (callback == null) { + callback = function(err, linkedFileData) {} + } + return this._checkAuth( + project_id, + linkedFileData, + user_id, + (err, allowed) => { + if (err != null) { + return callback(err) + } + if (!allowed) { + return callback(new AccessDeniedError()) + } + if (!this._validate(linkedFileData)) { + return callback(new BadDataError()) + } + return callback(null, linkedFileData) + } + ) + }, + + _go(project_id, linkedFileData, name, parent_folder_id, user_id, callback) { + linkedFileData = this._sanitizeData(linkedFileData) + return this._prepare( + project_id, + linkedFileData, + user_id, + (err, linkedFileData) => { + if (err != null) { + return callback(err) + } + if (!this._validate(linkedFileData)) { + return callback(new BadDataError()) + } + return this._getEntity( + linkedFileData, + user_id, + (err, source_project, entity, type) => { + if (err != null) { + return callback(err) + } + if (type === 'doc') { + return DocstoreManager.getDoc( + source_project._id, + entity._id, + function(err, lines) { + if (err != null) { + return callback(err) + } + return LinkedFilesHandler.importContent( + project_id, + lines.join('\n'), + linkedFileData, + name, + parent_folder_id, + user_id, + function(err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) + } + ) // Created + } else if (type === 'file') { + return FileStoreHandler.getFileStream( + source_project._id, + entity._id, + null, + function(err, fileStream) { + if (err != null) { + return callback(err) + } + return LinkedFilesHandler.importFromStream( + project_id, + fileStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function(err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) + } + ) // Created + } else { + return callback(new BadEntityTypeError()) + } + } + ) + } + ) + }, + + _getEntity(linkedFileData, current_user_id, callback) { + if (callback == null) { + callback = function(err, entity, type) {} + } + callback = _.once(callback) + const { source_entity_path } = linkedFileData + return this._getSourceProject(linkedFileData, function(err, project) { + if (err != null) { + return callback(err) + } + const source_project_id = project._id + return DocumentUpdaterHandler.flushProjectToMongo( + source_project_id, + function(err) { + if (err != null) { + return callback(err) + } + return ProjectLocator.findElementByPath( + { + project_id: source_project_id, + path: source_entity_path, + exactCaseMatch: true + }, + function(err, entity, type) { + if (err != null) { + if (/^not found.*/.test(err.toString())) { + err = new SourceFileNotFoundError() + } + return callback(err) + } + return callback(null, project, entity, type) + } + ) + } + ) + }) + }, + + _sanitizeData(data) { + return _.pick( + data, + 'provider', + 'source_project_id', + 'v1_source_doc_id', + 'source_entity_path' + ) + }, + + _validate(data) { + return ( + (data.source_project_id != null || data.v1_source_doc_id != null) && + data.source_entity_path != null + ) + }, + + _canCreate(data) { + // Don't allow creation of linked-files with v1 doc ids + return data.v1_source_doc_id == null + }, + + _getSourceProject: LinkedFilesHandler.getSourceProject, + + _checkAuth(project_id, data, current_user_id, callback) { + if (callback == null) { + callback = function(error, allowed) {} + } + callback = _.once(callback) + if (!ProjectFileAgent._validate(data)) { + return callback(new BadDataError()) + } + return this._getSourceProject(data, function(err, project) { + if (err != null) { + return callback(err) + } + return AuthorizationManager.canUserReadProject( + current_user_id, + project._id, + null, + function(err, canRead) { + if (err != null) { + return callback(err) + } + return callback(null, canRead) + } + ) + }) + } +} diff --git a/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js b/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js new file mode 100644 index 0000000000..cd77ab59cf --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js @@ -0,0 +1,301 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectOutputFileAgent +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const ProjectGetter = require('../Project/ProjectGetter') +const Settings = require('settings-sharelatex') +const CompileManager = require('../Compile/CompileManager') +const ClsiManager = require('../Compile/ClsiManager') +const ProjectFileAgent = require('./ProjectFileAgent') +const _ = require('underscore') +const { + BadDataError, + AccessDeniedError, + BadEntityTypeError, + OutputFileFetchFailedError +} = require('./LinkedFilesErrors') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const logger = require('logger-sharelatex') + +module.exports = ProjectOutputFileAgent = { + _prepare(project_id, linkedFileData, user_id, callback) { + if (callback == null) { + callback = function(err, linkedFileData) {} + } + return this._checkAuth( + project_id, + linkedFileData, + user_id, + (err, allowed) => { + if (err != null) { + return callback(err) + } + if (!allowed) { + return callback(new AccessDeniedError()) + } + if (!this._validate(linkedFileData)) { + return callback(new BadDataError()) + } + return callback(null, linkedFileData) + } + ) + }, + + createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (!this._canCreate(linkedFileData)) { + return callback(new AccessDeniedError()) + } + linkedFileData = this._sanitizeData(linkedFileData) + return this._prepare( + project_id, + linkedFileData, + user_id, + (err, linkedFileData) => { + if (err != null) { + return callback(err) + } + return this._getFileStream( + linkedFileData, + user_id, + (err, readStream) => { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + return readStream.on('response', response => { + if (response.statusCode >= 200 && response.statusCode < 300) { + readStream.resume() + return LinkedFilesHandler.importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function(err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) // Created + } else { + err = new OutputFileFetchFailedError( + `Output file fetch failed: ${linkedFileData.build_id}, ${ + linkedFileData.source_output_file_path + }` + ) + err.statusCode = response.statusCode + return callback(err) + } + }) + } + ) + } + ) + }, + + refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + return this._prepare( + project_id, + linkedFileData, + user_id, + (err, linkedFileData) => { + if (err != null) { + return callback(err) + } + return this._compileAndGetFileStream( + linkedFileData, + user_id, + (err, readStream, new_build_id) => { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + return readStream.on('response', response => { + if (response.statusCode >= 200 && response.statusCode < 300) { + readStream.resume() + linkedFileData.build_id = new_build_id + return LinkedFilesHandler.importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function(err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) // Created + } else { + err = new OutputFileFetchFailedError( + `Output file fetch failed: ${linkedFileData.build_id}, ${ + linkedFileData.source_output_file_path + }` + ) + err.statusCode = response.statusCode + return callback(err) + } + }) + } + ) + } + ) + }, + + _sanitizeData(data) { + return { + provider: data.provider, + source_project_id: data.source_project_id, + source_output_file_path: data.source_output_file_path, + build_id: data.build_id + } + }, + + _canCreate: ProjectFileAgent._canCreate, + + _getSourceProject: LinkedFilesHandler.getSourceProject, + + _validate(data) { + if (data.v1_source_doc_id != null) { + return ( + data.v1_source_doc_id != null && data.source_output_file_path != null + ) + } else { + return ( + data.source_project_id != null && + data.source_output_file_path != null && + data.build_id != null + ) + } + }, + + _checkAuth(project_id, data, current_user_id, callback) { + if (callback == null) { + callback = function(err, allowed) {} + } + callback = _.once(callback) + if (!this._validate(data)) { + return callback(new BadDataError()) + } + return this._getSourceProject(data, function(err, project) { + if (err != null) { + return callback(err) + } + return AuthorizationManager.canUserReadProject( + current_user_id, + project._id, + null, + function(err, canRead) { + if (err != null) { + return callback(err) + } + return callback(null, canRead) + } + ) + }) + }, + + _getFileStream(linkedFileData, user_id, callback) { + if (callback == null) { + callback = function(err, fileStream) {} + } + callback = _.once(callback) + const { source_output_file_path, build_id } = linkedFileData + return this._getSourceProject(linkedFileData, function(err, project) { + if (err != null) { + return callback(err) + } + const source_project_id = project._id + return ClsiManager.getOutputFileStream( + source_project_id, + user_id, + build_id, + source_output_file_path, + function(err, readStream) { + if (err != null) { + return callback(err) + } + readStream.pause() + return callback(null, readStream) + } + ) + }) + }, + + _compileAndGetFileStream(linkedFileData, user_id, callback) { + if (callback == null) { + callback = function(err, stream, build_id) {} + } + callback = _.once(callback) + const { source_output_file_path } = linkedFileData + return this._getSourceProject(linkedFileData, function(err, project) { + if (err != null) { + return callback(err) + } + const source_project_id = project._id + return CompileManager.compile(source_project_id, user_id, {}, function( + err, + status, + outputFiles + ) { + if (err != null) { + return callback(err) + } + if (status !== 'success') { + return callback(new OutputFileFetchFailedError()) + } + const outputFile = _.find( + outputFiles, + o => o.path === source_output_file_path + ) + if (outputFile == null) { + return callback(new OutputFileFetchFailedError()) + } + const build_id = outputFile.build + return ClsiManager.getOutputFileStream( + source_project_id, + user_id, + build_id, + source_output_file_path, + function(err, readStream) { + if (err != null) { + return callback(err) + } + readStream.pause() + return callback(null, readStream, build_id) + } + ) + }) + }) + } +} diff --git a/services/web/app/src/Features/LinkedFiles/UrlAgent.js b/services/web/app/src/Features/LinkedFiles/UrlAgent.js new file mode 100644 index 0000000000..fd023efe84 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/UrlAgent.js @@ -0,0 +1,108 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UrlAgent +const request = require('request') +const _ = require('underscore') +const urlValidator = require('valid-url') +const { InvalidUrlError, UrlFetchFailedError } = require('./LinkedFilesErrors') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const UrlHelper = require('../Helpers/UrlHelper') + +module.exports = UrlAgent = { + createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + linkedFileData = this._sanitizeData(linkedFileData) + return this._getUrlStream(project_id, linkedFileData, user_id, function( + err, + readStream + ) { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + return readStream.on('response', function(response) { + if (response.statusCode >= 200 && response.statusCode < 300) { + readStream.resume() + return LinkedFilesHandler.importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function(err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) // Created + } else { + const error = new UrlFetchFailedError( + `url fetch failed: ${linkedFileData.url}` + ) + error.statusCode = response.statusCode + return callback(error) + } + }) + }) + }, + + refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + return this.createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) + }, + + _sanitizeData(data) { + return { + provider: data.provider, + url: UrlHelper.prependHttpIfNeeded(data.url) + } + }, + + _getUrlStream(project_id, data, current_user_id, callback) { + if (callback == null) { + callback = function(error, fsPath) {} + } + callback = _.once(callback) + let { url } = data + if (!urlValidator.isWebUri(url)) { + return callback(new InvalidUrlError(`invalid url: ${url}`)) + } + url = UrlHelper.wrapUrlWithProxy(url) + const readStream = request.get(url) + readStream.pause() + return callback(null, readStream) + } +} diff --git a/services/web/app/src/Features/Metadata/MetaController.js b/services/web/app/src/Features/Metadata/MetaController.js new file mode 100644 index 0000000000..5c3065382d --- /dev/null +++ b/services/web/app/src/Features/Metadata/MetaController.js @@ -0,0 +1,60 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MetaController +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const MetaHandler = require('./MetaHandler') +const logger = require('logger-sharelatex') + +module.exports = MetaController = { + getMetadata(req, res, next) { + const { project_id } = req.params + logger.log({ project_id }, 'getting all labels for project') + return MetaHandler.getAllMetaForProject(project_id, function( + err, + projectMeta + ) { + if (err != null) { + logger.err( + { project_id, err }, + '[MetaController] error getting all labels from project' + ) + return next(err) + } + return res.json({ projectId: project_id, projectMeta }) + }) + }, + + broadcastMetadataForDoc(req, res, next) { + const { project_id } = req.params + const { doc_id } = req.params + logger.log({ project_id, doc_id }, 'getting labels for doc') + return MetaHandler.getMetaForDoc(project_id, doc_id, function( + err, + docMeta + ) { + if (err != null) { + logger.err( + { project_id, doc_id, err }, + '[MetaController] error getting labels from doc' + ) + return next(err) + } + EditorRealTimeController.emitToRoom(project_id, 'broadcastDocMeta', { + docId: doc_id, + meta: docMeta + }) + return res.sendStatus(200) + }) + } +} diff --git a/services/web/app/src/Features/Metadata/MetaHandler.js b/services/web/app/src/Features/Metadata/MetaHandler.js new file mode 100644 index 0000000000..25dc323b04 --- /dev/null +++ b/services/web/app/src/Features/Metadata/MetaHandler.js @@ -0,0 +1,126 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-cond-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MetaHandler +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const packageMapping = require('./packageMapping') + +module.exports = MetaHandler = { + labelRegex() { + return /\\label{(.{0,80}?)}/g + }, + + usepackageRegex() { + return /^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/g + }, + + ReqPackageRegex() { + return /^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/g + }, + + getAllMetaForProject(projectId, callback) { + if (callback == null) { + callback = function(err, projectMeta) {} + } + return DocumentUpdaterHandler.flushProjectToMongo(projectId, function(err) { + if (err != null) { + return callback(err) + } + return ProjectEntityHandler.getAllDocs(projectId, function(err, docs) { + if (err != null) { + return callback(err) + } + const projectMeta = MetaHandler.extractMetaFromProjectDocs(docs) + return callback(null, projectMeta) + }) + }) + }, + + getMetaForDoc(projectId, docId, callback) { + if (callback == null) { + callback = function(err, docMeta) {} + } + return DocumentUpdaterHandler.flushDocToMongo(projectId, docId, function( + err + ) { + if (err != null) { + return callback(err) + } + return ProjectEntityHandler.getDoc(projectId, docId, function( + err, + lines + ) { + if (err != null) { + return callback(err) + } + const docMeta = MetaHandler.extractMetaFromDoc(lines) + return callback(null, docMeta) + }) + }) + }, + + extractMetaFromDoc(lines) { + let pkg + const docMeta = { labels: [], packages: {} } + const packages = [] + const label_re = MetaHandler.labelRegex() + const package_re = MetaHandler.usepackageRegex() + const req_package_re = MetaHandler.ReqPackageRegex() + for (let line of Array.from(lines)) { + var labelMatch + var clean, messy, packageMatch + while ((labelMatch = label_re.exec(line))) { + var label + if ((label = labelMatch[1])) { + docMeta.labels.push(label) + } + } + while ((packageMatch = package_re.exec(line))) { + if ((messy = packageMatch[1])) { + for (pkg of Array.from(messy.split(','))) { + if ((clean = pkg.trim())) { + packages.push(clean) + } + } + } + } + while ((packageMatch = req_package_re.exec(line))) { + if ((messy = packageMatch[1])) { + for (pkg of Array.from(messy.split(','))) { + if ((clean = pkg.trim())) { + packages.push(clean) + } + } + } + } + } + for (pkg of Array.from(packages)) { + if (packageMapping[pkg] != null) { + docMeta.packages[pkg] = packageMapping[pkg] + } + } + return docMeta + }, + + extractMetaFromProjectDocs(projectDocs) { + const projectMeta = {} + for (let _path in projectDocs) { + const doc = projectDocs[_path] + projectMeta[doc._id] = MetaHandler.extractMetaFromDoc(doc.lines) + } + return projectMeta + } +} diff --git a/services/web/app/src/Features/Metadata/packageMapping.js b/services/web/app/src/Features/Metadata/packageMapping.js new file mode 100644 index 0000000000..30a294d158 --- /dev/null +++ b/services/web/app/src/Features/Metadata/packageMapping.js @@ -0,0 +1,71595 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +module.exports = { + inputenc: [ + { + caption: '\\inputencoding{}', + snippet: '\\inputencoding{$1}', + meta: 'inputenc-cmd', + score: 0.0002447047447770061 + } + ], + graphicx: [ + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphicx-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphicx-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'graphicx-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'graphicx-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'graphicx-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'graphicx-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'graphicx-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'graphicx-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'graphicx-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'graphicx-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphicx-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'graphicx-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphicx-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphicx-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'graphicx-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphicx-cmd', + score: 0.008565354665444157 + } + ], + amsmath: [ + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'amsmath-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'amsmath-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'amsmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'amsmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'amsmath-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'amsmath-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'amsmath-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'amsmath-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'amsmath-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'amsmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'amsmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'amsmath-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'amsmath-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'amsmath-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'amsmath-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'amsmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'amsmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'amsmath-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'amsmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'amsmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'amsmath-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'amsmath-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'amsmath-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'amsmath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'amsmath-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'amsmath-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'amsmath-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'amsmath-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'amsmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'amsmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'amsmath-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'amsmath-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'amsmath-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'amsmath-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'amsmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'amsmath-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'amsmath-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'amsmath-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'amsmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'amsmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'amsmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'amsmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'amsmath-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'amsmath-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'amsmath-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'amsmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'amsmath-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'amsmath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'amsmath-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'amsmath-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'amsmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'amsmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'amsmath-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'amsmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'amsmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'amsmath-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'amsmath-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'amsmath-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'amsmath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'amsmath-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'amsmath-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'amsmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'amsmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'amsmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'amsmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'amsmath-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'amsmath-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'amsmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'amsmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'amsmath-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'amsmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'amsmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'amsmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'amsmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'amsmath-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'amsmath-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'amsmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'amsmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'amsmath-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'amsmath-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'amsmath-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'amsmath-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'amsmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'amsmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'amsmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'amsmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'amsmath-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'amsmath-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'amsmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'amsmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'amsmath-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'amsmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'amsmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'amsmath-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'amsmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'amsmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'amsmath-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'amsmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'amsmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'amsmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'amsmath-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'amsmath-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'amsmath-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'amsmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'amsmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'amsmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'amsmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'amsmath-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'amsmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'amsmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'amsmath-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'amsmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'amsmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'amsmath-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'amsmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'amsmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'amsmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'amsmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'amsmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'amsmath-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'amsmath-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'amsmath-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'amsmath-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'amsmath-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'amsmath-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'amsmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'amsmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'amsmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'amsmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'amsmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'amsmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'amsmath-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'amsmath-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'amsmath-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'amsmath-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'amsmath-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'amsmath-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'amsmath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsmath-cmd', + score: 0.0063276692758974925 + } + ], + geometry: [ + { + caption: '\\savegeometry{}', + snippet: '\\savegeometry{$1}', + meta: 'geometry-cmd', + score: 6.461638865465447e-5 + }, + { + caption: '\\loadgeometry{}', + snippet: '\\loadgeometry{$1}', + meta: 'geometry-cmd', + score: 6.461638865465447e-5 + }, + { + caption: '\\newgeometry{}', + snippet: '\\newgeometry{$1}', + meta: 'geometry-cmd', + score: 0.0025977479207639352 + }, + { + caption: '\\geometry{}', + snippet: '\\geometry{$1}', + meta: 'geometry-cmd', + score: 0.046218420429973615 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'geometry-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\restoregeometry', + snippet: '\\restoregeometry', + meta: 'geometry-cmd', + score: 0.0007546303842143648 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'geometry-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'geometry-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'geometry-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'geometry-cmd', + score: 0.00021116765384691477 + } + ], + amssymb: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'amssymb-cmd', + score: 0.0017966000518546787 + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'amssymb-cmd', + score: 0.025060530944368123 + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'amssymb-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'amssymb-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'amssymb-cmd', + score: 0.0006671850995492977 + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'amssymb-cmd', + score: 0.0006671850995492977 + } + ], + hyperref: [ + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'hyperref-cmd', + score: 0.009472569279662113 + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'hyperref-cmd', + score: 0.006492248863367502 + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'hyperref-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'hyperref-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'hyperref-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hyperref-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'hyperref-cmd', + score: 0.017289599800633146 + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'hyperref-cmd', + score: 0.001509072212764015 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'hyperref-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'hyperref-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'hyperref-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'hyperref-cmd', + score: 7.849662248028187 + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.00978652043902115 + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'hyperref-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'hyperref-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'hyperref-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'hyperref-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'hyperref-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'hyperref-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'hyperref-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.00029737672328168955 + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.0073781967296121 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'hyperref-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'hyperref-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'hyperref-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'hyperref-cmd', + score: 0.019788865471151957 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'hyperref-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'hyperref-cmd', + score: 3.800886892251021 + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'hyperref-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'hyperref-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'hyperref-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'hyperref-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'hyperref-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'hyperref-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'hyperref-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'hyperref-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.009652820108904094 + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'hyperref-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'hyperref-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'hyperref-cmd', + score: 0.13586474005868793 + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'hyperref-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'hyperref-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.27111130260612365 + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'hyperref-cmd', + score: 0.0038703587462843594 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'hyperref-cmd', + score: 0.03741172773691362 + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'hyperref-cmd', + score: 0.0004995635515943437 + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'hyperref-cmd', + score: 7.847906405228455 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'hyperref-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'hyperref-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'hyperref-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'hyperref-cmd', + score: 6.006262128895586e-5 + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'hyperref-cmd', + score: 0.00017906650306643613 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'hyperref-cmd', + score: 1.4380093454211778 + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'hyperref-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'hyperref-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'hyperref-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'hyperref-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'hyperref-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'hyperref-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'hyperref-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'hyperref-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'hyperref-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'hyperref-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'hyperref-cmd', + score: 0.004515152477030062 + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'hyperref-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'hyperref-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'hyperref-cmd', + score: 0.3311721696201715 + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'hyperref-cmd', + score: 0.06967310843464661 + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'hyperref-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'hyperref-cmd', + score: 0.9202908262245683 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'hyperref-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'hyperref-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'hyperref-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'hyperref-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'hyperref-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'hyperref-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'hyperref-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'hyperref-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'hyperref-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'hyperref-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'hyperref-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'hyperref-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'hyperref-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'hyperref-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'hyperref-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'hyperref-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411 + } + ], + babel: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'babel-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'babel-cmd', + score: 0.021170869458413965 + } + ], + color: [ + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'color-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'color-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'color-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'color-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'color-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'color-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'color-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'color-cmd', + score: 0.2864294797053033 + } + ], + xcolor: [ + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xcolor-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xcolor-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xcolor-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xcolor-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xcolor-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xcolor-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xcolor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xcolor-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xcolor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xcolor-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xcolor-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xcolor-cmd', + score: 0.2864294797053033 + } + ], + natbib: [ + { + caption: '\\citealt{}', + snippet: '\\citealt{$1}', + meta: 'natbib-cmd', + score: 0.007302105441724955 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'natbib-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'natbib-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'natbib-cmd', + score: 0.05216393882408519 + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'natbib-cmd', + score: 0.04990693820960752 + }, + { + caption: '\\bibname', + snippet: '\\bibname', + meta: 'natbib-cmd', + score: 0.007599529252128519 + }, + { + caption: '\\bibname{}', + snippet: '\\bibname{$1}', + meta: 'natbib-cmd', + score: 0.007599529252128519 + }, + { + caption: '\\bibpunct', + snippet: '\\bibpunct', + meta: 'natbib-cmd', + score: 0.001148574749873469 + }, + { + caption: '\\bibpunct{}{}{}{}{}{}', + snippet: '\\bibpunct{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'natbib-cmd', + score: 0.001148574749873469 + }, + { + caption: '\\bibpunct[]{}{}{}{}{}{}', + snippet: '\\bibpunct[$1]{$2}{$3}{$4}{$5}{$6}{$7}', + meta: 'natbib-cmd', + score: 0.001148574749873469 + }, + { + caption: '\\citepalias{}', + snippet: '\\citepalias{$1}', + meta: 'natbib-cmd', + score: 0.00032712684909035603 + }, + { + caption: '\\citepalias[][]{}', + snippet: '\\citepalias[$1][$2]{$3}', + meta: 'natbib-cmd', + score: 0.00032712684909035603 + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'natbib-cmd', + score: 0.010304996748556729 + }, + { + caption: '\\citep{}', + snippet: '\\citep{$1}', + meta: 'natbib-cmd', + score: 0.2941882834697057 + }, + { + caption: '\\bibsection', + snippet: '\\bibsection', + meta: 'natbib-cmd', + score: 0.00038872734530908233 + }, + { + caption: '\\bibsection{}', + snippet: '\\bibsection{$1}', + meta: 'natbib-cmd', + score: 0.00038872734530908233 + }, + { + caption: '\\refname', + snippet: '\\refname', + meta: 'natbib-cmd', + score: 0.006490238196722249 + }, + { + caption: '\\refname{}', + snippet: '\\refname{$1}', + meta: 'natbib-cmd', + score: 0.006490238196722249 + }, + { + caption: '\\citealp{}', + snippet: '\\citealp{$1}', + meta: 'natbib-cmd', + score: 0.005275912376595364 + }, + { + caption: '\\citealp[]{}', + snippet: '\\citealp[$1]{$2}', + meta: 'natbib-cmd', + score: 0.005275912376595364 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'natbib-cmd', + score: 2.341195220791228 + }, + { + caption: '\\citetalias{}', + snippet: '\\citetalias{$1}', + meta: 'natbib-cmd', + score: 0.001419571355756266 + }, + { + caption: '\\bibitem{}', + snippet: '\\bibitem{$1}', + meta: 'natbib-cmd', + score: 0.3689547570562042 + }, + { + caption: '\\bibitem[]{}', + snippet: '\\bibitem[$1]{$2}', + meta: 'natbib-cmd', + score: 0.3689547570562042 + }, + { + caption: '\\citet{}', + snippet: '\\citet{$1}', + meta: 'natbib-cmd', + score: 0.09046048561361801 + }, + { + caption: '\\defcitealias{}{}', + snippet: '\\defcitealias{$1}{$2}', + meta: 'natbib-cmd', + score: 0.00042021825647418025 + }, + { + caption: '\\aftergroup', + snippet: '\\aftergroup', + meta: 'natbib-cmd', + score: 0.002020423627422133 + }, + { + caption: '\\setcitestyle{}', + snippet: '\\setcitestyle{$1}', + meta: 'natbib-cmd', + score: 0.0015840652870152204 + }, + { + caption: '\\citeyearpar{}', + snippet: '\\citeyearpar{$1}', + meta: 'natbib-cmd', + score: 0.001877888310324327 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'natbib-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'natbib-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\newblock', + snippet: '\\newblock', + meta: 'natbib-cmd', + score: 0.03684301726876973 + }, + { + caption: '\\newblock{}', + snippet: '\\newblock{$1}', + meta: 'natbib-cmd', + score: 0.03684301726876973 + }, + { + caption: '\\bibnumfmt', + snippet: '\\bibnumfmt', + meta: 'natbib-cmd', + score: 0.000353353600267394 + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'natbib-cmd', + score: 0.01091041305836494 + }, + { + caption: '\\citeauthor{}', + snippet: '\\citeauthor{$1}', + meta: 'natbib-cmd', + score: 0.01359248786373484 + } + ], + url: [ + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'url-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'url-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'url-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'url-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'url-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'url-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'url-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'url-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'url-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'url-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'url-cmd', + score: 3.7048287721105874e-5 + } + ], + fontenc: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fontenc-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fontenc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fontenc-cmd', + score: 0.021170869458413965 + } + ], + tikz: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-cmd', + score: 0.2864294797053033 + } + ], + fancyhdr: [ + { + caption: '\\lhead{}', + snippet: '\\lhead{$1}', + meta: 'fancyhdr-cmd', + score: 0.05268978171228714 + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'fancyhdr-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'fancyhdr-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\fancypagestyle{}{}', + snippet: '\\fancypagestyle{$1}{$2}', + meta: 'fancyhdr-cmd', + score: 0.009430919590937878 + }, + { + caption: '\\footrule', + snippet: '\\footrule', + meta: 'fancyhdr-cmd', + score: 0.0010032754348913366 + }, + { + caption: '\\footrule{}', + snippet: '\\footrule{$1}', + meta: 'fancyhdr-cmd', + score: 0.0010032754348913366 + }, + { + caption: '\\fancyfoot[]{}', + snippet: '\\fancyfoot[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.024973618823189894 + }, + { + caption: '\\fancyfoot{}', + snippet: '\\fancyfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.024973618823189894 + }, + { + caption: '\\fancyfootoffset[]{}', + snippet: '\\fancyfootoffset[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.0015373246231684555 + }, + { + caption: '\\fancyfootoffset{}', + snippet: '\\fancyfootoffset{$1}', + meta: 'fancyhdr-cmd', + score: 0.0015373246231684555 + }, + { + caption: '\\footruleskip', + snippet: '\\footruleskip', + meta: 'fancyhdr-cmd', + score: 0.000830117957327721 + }, + { + caption: '\\fancyheadoffset[]{}', + snippet: '\\fancyheadoffset[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.0016786568695309166 + }, + { + caption: '\\fancyheadoffset{}', + snippet: '\\fancyheadoffset{$1}', + meta: 'fancyhdr-cmd', + score: 0.0016786568695309166 + }, + { + caption: '\\iffloatpage{}{}', + snippet: '\\iffloatpage{$1}{$2}', + meta: 'fancyhdr-cmd', + score: 6.606286310833368e-5 + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.013411641301057813 + }, + { + caption: '\\subsectionmark', + snippet: '\\subsectionmark', + meta: 'fancyhdr-cmd', + score: 3.1153423008593836e-5 + }, + { + caption: '\\footrulewidth', + snippet: '\\footrulewidth', + meta: 'fancyhdr-cmd', + score: 0.011424740897486949 + }, + { + caption: '\\fancyhfoffset[]{}', + snippet: '\\fancyhfoffset[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 3.741978601121172e-5 + }, + { + caption: '\\rhead{}', + snippet: '\\rhead{$1}', + meta: 'fancyhdr-cmd', + score: 0.022782817416731292 + }, + { + caption: '\\fancyplain{}{}', + snippet: '\\fancyplain{$1}{$2}', + meta: 'fancyhdr-cmd', + score: 0.007402339896386138 + }, + { + caption: '\\rfoot{}', + snippet: '\\rfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.013393817825547868 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fancyhdr-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\plainheadrulewidth', + snippet: '\\plainheadrulewidth', + meta: 'fancyhdr-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'fancyhdr-cmd', + score: 0.03225350148161425 + }, + { + caption: '\\lfoot{}', + snippet: '\\lfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.00789399846642229 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'fancyhdr-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'fancyhdr-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\fancyhf{}', + snippet: '\\fancyhf{$1}', + meta: 'fancyhdr-cmd', + score: 0.02314618933449356 + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'fancyhdr-cmd', + score: 0.005008938879210868 + }, + { + caption: '\\fancyhead[]{}', + snippet: '\\fancyhead[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.039101068064744296 + }, + { + caption: '\\fancyhead{}', + snippet: '\\fancyhead{$1}', + meta: 'fancyhdr-cmd', + score: 0.039101068064744296 + }, + { + caption: '\\nouppercase{}', + snippet: '\\nouppercase{$1}', + meta: 'fancyhdr-cmd', + score: 0.006416387071584083 + }, + { + caption: '\\nouppercase', + snippet: '\\nouppercase', + meta: 'fancyhdr-cmd', + score: 0.006416387071584083 + }, + { + caption: '\\headrule', + snippet: '\\headrule', + meta: 'fancyhdr-cmd', + score: 0.0008327432627715623 + }, + { + caption: '\\headrule{}', + snippet: '\\headrule{$1}', + meta: 'fancyhdr-cmd', + score: 0.0008327432627715623 + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'fancyhdr-cmd', + score: 0.00755042164734884 + }, + { + caption: '\\headrulewidth', + snippet: '\\headrulewidth', + meta: 'fancyhdr-cmd', + score: 0.02268137935335823 + } + ], + booktabs: [ + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'booktabs-cmd', + score: 0.004974385202605165 + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'booktabs-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'booktabs-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'booktabs-cmd', + score: 0.04533364657852219 + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'booktabs-cmd', + score: 0.07098077735912875 + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'booktabs-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'booktabs-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'booktabs-cmd', + score: 0.059857788139528495 + } + ], + amsfonts: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'amsfonts-cmd', + score: 0.0017966000518546787 + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'amsfonts-cmd', + score: 0.025060530944368123 + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'amsfonts-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'amsfonts-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'amsfonts-cmd', + score: 0.0006671850995492977 + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'amsfonts-cmd', + score: 0.0006671850995492977 + } + ], + float: [ + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'float-cmd', + score: 0.0009837365348002915 + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'float-cmd', + score: 0.0005815474978918903 + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'float-cmd', + score: 0.0008866338267686714 + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'float-cmd', + score: 0.0015470917047414941 + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'float-cmd', + score: 0.0011934321931750752 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'float-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'float-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'float-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'float-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'float-cmd', + score: 0.0012745874472536625 + } + ], + amsthm: [ + { + caption: '\\swapnumbers', + snippet: '\\swapnumbers', + meta: 'amsthm-cmd', + score: 0.0002908376412221364 + }, + { + caption: '\\qedhere', + snippet: '\\qedhere', + meta: 'amsthm-cmd', + score: 0.0001608548097938035 + }, + { + caption: '\\qed', + snippet: '\\qed', + meta: 'amsthm-cmd', + score: 0.0014240748825867814 + }, + { + caption: '\\qed{}', + snippet: '\\qed{$1}', + meta: 'amsthm-cmd', + score: 0.0014240748825867814 + }, + { + caption: '\\newtheoremstyle{}', + snippet: '\\newtheoremstyle{$1}', + meta: 'amsthm-cmd', + score: 0.004259886909451789 + }, + { + caption: '\\newtheoremstyle{}{}{}', + snippet: '\\newtheoremstyle{$1}{$2}{$3}', + meta: 'amsthm-cmd', + score: 0.004259886909451789 + }, + { + caption: '\\newtheoremstyle{}{}{}{}', + snippet: '\\newtheoremstyle{$1}{$2}{$3}{$4}', + meta: 'amsthm-cmd', + score: 0.004259886909451789 + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'amsthm-cmd', + score: 0.02533412165007986 + }, + { + caption: '\\proofname', + snippet: '\\proofname', + meta: 'amsthm-cmd', + score: 0.00021208362094925234 + }, + { + caption: '\\pushQED{}', + snippet: '\\pushQED{$1}', + meta: 'amsthm-cmd', + score: 0.00019346981338869148 + }, + { + caption: '\\qedsymbol', + snippet: '\\qedsymbol', + meta: 'amsthm-cmd', + score: 0.0022671784428571723 + }, + { + caption: '\\qedsymbol{}', + snippet: '\\qedsymbol{$1}', + meta: 'amsthm-cmd', + score: 0.0022671784428571723 + }, + { + caption: '\\popQED', + snippet: '\\popQED', + meta: 'amsthm-cmd', + score: 9.673490669434574e-5 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'amsthm-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'amsthm-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'amsthm-cmd', + score: 0.215689795055434 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsthm-cmd', + score: 0.0063276692758974925 + } + ], + caption: [ + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'caption-cmd', + score: 0.018348594199161503 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'caption-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'caption-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'caption-cmd', + score: 0.422097569591803 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'caption-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'caption-cmd', + score: 0.3147206476372336 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'caption-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'caption-cmd', + score: 1.897791904799601 + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'caption-cmd', + score: 5.806935368083486e-5 + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'caption-cmd', + score: 0.42355747798114207 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'caption-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'caption-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'caption-cmd', + score: 0.0003890810058478364 + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'caption-cmd', + score: 0.0004717618449370015 + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'caption-cmd', + score: 5.0133404990680195e-5 + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'caption-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'caption-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'caption-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'caption-cmd', + score: 0.00015256647321237863 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'caption-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'caption-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'caption-cmd', + score: 0.021473212893597875 + } + ], + ifthen: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'ifthen-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'ifthen-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'ifthen-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'ifthen-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'ifthen-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'ifthen-cmd', + score: 0.0018957469739775527 + } + ], + setspace: [ + { + caption: '\\setstretch{}', + snippet: '\\setstretch{$1}', + meta: 'setspace-cmd', + score: 0.019634763572332112 + }, + { + caption: '\\onehalfspacing', + snippet: '\\onehalfspacing', + meta: 'setspace-cmd', + score: 0.010655415521079565 + }, + { + caption: '\\singlespacing', + snippet: '\\singlespacing', + meta: 'setspace-cmd', + score: 0.008351544612280968 + }, + { + caption: '\\doublespacing', + snippet: '\\doublespacing', + meta: 'setspace-cmd', + score: 0.007835428951987135 + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'setspace-cmd', + score: 0.03225350148161425 + } + ], + multirow: [ + { + caption: '\\multirow{}{}{}', + snippet: '\\multirow{$1}{$2}{$3}', + meta: 'multirow-cmd', + score: 0.07525389638751734 + }, + { + caption: '\\multirow{}[]{}{}', + snippet: '\\multirow{$1}[$2]{$3}{$4}', + meta: 'multirow-cmd', + score: 0.07525389638751734 + } + ], + array: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'array-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'array-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'array-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'array-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'array-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'array-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'array-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'array-cmd', + score: 0.018615449342361392 + } + ], + titlesec: [ + { + caption: '\\titleclass{}{}[]', + snippet: '\\titleclass{$1}{$2}[$3]', + meta: 'titlesec-cmd', + score: 0.00028979763314974667 + }, + { + caption: '\\titlelabel{}', + snippet: '\\titlelabel{$1}', + meta: 'titlesec-cmd', + score: 6.40387839367932e-6 + }, + { + caption: '\\thetitle', + snippet: '\\thetitle', + meta: 'titlesec-cmd', + score: 0.0015531478302713473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'titlesec-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'titlesec-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\titleformat{}{}{}{}{}[]', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]', + meta: 'titlesec-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}[]{}{}{}{}', + snippet: '\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'titlesec-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}{}', + snippet: '\\titleformat{$1}{$2}', + meta: 'titlesec-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}{}{}{}{}', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}', + meta: 'titlesec-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titlespacing{}{}{}{}', + snippet: '\\titlespacing{$1}{$2}{$3}{$4}', + meta: 'titlesec-cmd', + score: 0.023062744385192156 + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'titlesec-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'titlesec-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'titlesec-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'titlesec-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\filleft', + snippet: '\\filleft', + meta: 'titlesec-cmd', + score: 7.959989906732799e-5 + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'titlesec-cmd', + score: 0.0004835660211260246 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'titlesec-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'titlesec-cmd', + score: 0.044016804142963585 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'titlesec-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\chaptertitlename', + snippet: '\\chaptertitlename', + meta: 'titlesec-cmd', + score: 0.0016985007766926272 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'titlesec-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\filright', + snippet: '\\filright', + meta: 'titlesec-cmd', + score: 7.959989906732799e-5 + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'titlesec-cmd', + score: 0.019273712561461216 + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'titlesec-cmd', + score: 0.019273712561461216 + } + ], + multicol: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'multicol-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'multicol-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\raggedcolumns', + snippet: '\\raggedcolumns', + meta: 'multicol-cmd', + score: 0.00027461965178228156 + }, + { + caption: '\\columnbreak', + snippet: '\\columnbreak', + meta: 'multicol-cmd', + score: 0.002609610141555795 + }, + { + caption: '\\columnseprulecolor{}', + snippet: '\\columnseprulecolor{$1}', + meta: 'multicol-cmd', + score: 1.3314892207625771e-5 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'multicol-cmd', + score: 0.1789117552185788 + } + ], + listings: [ + { + caption: '\\vskip', + snippet: '\\vskip', + meta: 'listings-cmd', + score: 0.05143052892347224 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'listings-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'listings-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'listings-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\thelstlisting', + snippet: '\\thelstlisting', + meta: 'listings-cmd', + score: 0.00012774128088872144 + }, + { + caption: '\\lstinputlisting[]{}', + snippet: '\\lstinputlisting[$1]{$2}', + meta: 'listings-cmd', + score: 0.011660477607086044 + }, + { + caption: '\\lstinputlisting{}', + snippet: '\\lstinputlisting{$1}', + meta: 'listings-cmd', + score: 0.011660477607086044 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'listings-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'listings-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\lstinline', + snippet: '\\lstinline', + meta: 'listings-cmd', + score: 0.005972262850694285 + }, + { + caption: '\\lstinline{}', + snippet: '\\lstinline{$1}', + meta: 'listings-cmd', + score: 0.005972262850694285 + }, + { + caption: '\\lstlistoflistings', + snippet: '\\lstlistoflistings', + meta: 'listings-cmd', + score: 0.005279080363360602 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'listings-cmd', + score: 0.00037306820619479756 + } + ], + blindtext: [ + { + caption: '\\glqq', + snippet: '\\glqq', + meta: 'blindtext-cmd', + score: 0.0039133256714254504 + }, + { + caption: '\\glqq{}', + snippet: '\\glqq{$1}', + meta: 'blindtext-cmd', + score: 0.0039133256714254504 + }, + { + caption: '\\blindtext', + snippet: '\\blindtext', + meta: 'blindtext-cmd', + score: 0.05782040856823667 + }, + { + caption: '\\blindtext[]', + snippet: '\\blindtext[$1]', + meta: 'blindtext-cmd', + score: 0.05782040856823667 + }, + { + caption: '\\Blindtext', + snippet: '\\Blindtext', + meta: 'blindtext-cmd', + score: 0.006384906903938044 + }, + { + caption: '\\grqq', + snippet: '\\grqq', + meta: 'blindtext-cmd', + score: 0.006659522189248266 + }, + { + caption: '\\grqq{}', + snippet: '\\grqq{$1}', + meta: 'blindtext-cmd', + score: 0.006659522189248266 + }, + { + caption: '\\blinddocument', + snippet: '\\blinddocument', + meta: 'blindtext-cmd', + score: 0.00011480988129172825 + }, + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'blindtext-cmd', + score: 0.07560370351316588 + } + ], + enumitem: [ + { + caption: '\\newlist{}{}{}', + snippet: '\\newlist{$1}{$2}{$3}', + meta: 'enumitem-cmd', + score: 0.0007266225924074459 + }, + { + caption: '\\setlist[]{}', + snippet: '\\setlist[$1]{$2}', + meta: 'enumitem-cmd', + score: 0.010895384475728338 + }, + { + caption: '\\setlist{}', + snippet: '\\setlist{$1}', + meta: 'enumitem-cmd', + score: 0.010895384475728338 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'enumitem-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'enumitem-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\setlistdepth{}', + snippet: '\\setlistdepth{$1}', + meta: 'enumitem-cmd', + score: 0.0001113322912630871 + }, + { + caption: '\\setenumerate[]{}', + snippet: '\\setenumerate[$1]{$2}', + meta: 'enumitem-cmd', + score: 7.437178301071255e-5 + }, + { + caption: '\\setenumerate{}', + snippet: '\\setenumerate{$1}', + meta: 'enumitem-cmd', + score: 7.437178301071255e-5 + }, + { + caption: '\\renewlist{}{}{}', + snippet: '\\renewlist{$1}{$2}{$3}', + meta: 'enumitem-cmd', + score: 0.0001113322912630871 + }, + { + caption: '\\descriptionlabel{}', + snippet: '\\descriptionlabel{$1}', + meta: 'enumitem-cmd', + score: 7.678089052626698e-6 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'enumitem-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\setitemize[]{}', + snippet: '\\setitemize[$1]{$2}', + meta: 'enumitem-cmd', + score: 0.0019580640711971786 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'enumitem-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'enumitem-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\makelabel', + snippet: '\\makelabel', + meta: 'enumitem-cmd', + score: 5.739925426740175e-5 + }, + { + caption: '\\makelabel{}', + snippet: '\\makelabel{$1}', + meta: 'enumitem-cmd', + score: 5.739925426740175e-5 + }, + { + caption: '\\makelabel[]{}', + snippet: '\\makelabel[$1]{$2}', + meta: 'enumitem-cmd', + score: 5.739925426740175e-5 + } + ], + times: [ + { + caption: '\\rmdefault', + snippet: '\\rmdefault', + meta: 'times-cmd', + score: 0.0012870877747432935 + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'times-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'times-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\ttdefault', + snippet: '\\ttdefault', + meta: 'times-cmd', + score: 0.0011733254149332488 + }, + { + caption: '\\ttdefault{}', + snippet: '\\ttdefault{$1}', + meta: 'times-cmd', + score: 0.0011733254149332488 + } + ], + subcaption: [ + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subcaption-cmd', + score: 0.007192033516871399 + }, + { + caption: '\\subcaptionbox{}{}', + snippet: '\\subcaptionbox{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0008634329663023698 + }, + { + caption: '\\newsubfloat{}', + snippet: '\\newsubfloat{$1}', + meta: 'subcaption-cmd', + score: 0.000615805121082521 + }, + { + caption: '\\subcaption{}', + snippet: '\\subcaption{$1}', + meta: 'subcaption-cmd', + score: 0.006820005741581297 + }, + { + caption: '\\subcaption[]{}', + snippet: '\\subcaption[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.006820005741581297 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'subcaption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.018348594199161503 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'subcaption-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'subcaption-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'subcaption-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'subcaption-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'subcaption-cmd', + score: 0.422097569591803 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'subcaption-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'subcaption-cmd', + score: 0.3147206476372336 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'subcaption-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'subcaption-cmd', + score: 1.897791904799601 + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'subcaption-cmd', + score: 5.806935368083486e-5 + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'subcaption-cmd', + score: 0.42355747798114207 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0003890810058478364 + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0004717618449370015 + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'subcaption-cmd', + score: 5.0133404990680195e-5 + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'subcaption-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'subcaption-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'subcaption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'subcaption-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'subcaption-cmd', + score: 0.00015256647321237863 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'subcaption-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'subcaption-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'subcaption-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'subcaption-cmd', + score: 0.021473212893597875 + } + ], + bm: [ + { + caption: '\\bm{}', + snippet: '\\bm{$1}', + meta: 'bm-cmd', + score: 0.14733018077819282 + }, + { + caption: '\\bm', + snippet: '\\bm', + meta: 'bm-cmd', + score: 0.14733018077819282 + } + ], + fontspec: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'fontspec-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'fontspec-cmd', + score: 0.2864294797053033 + } + ], + subfigure: [ + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subfigure-cmd', + score: 0.007192033516871399 + }, + { + caption: '\\subfigure[]{}', + snippet: '\\subfigure[$1]{$2}', + meta: 'subfigure-cmd', + score: 0.037856842641104005 + } + ], + calc: [ + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'calc-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'calc-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'calc-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'calc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'calc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'calc-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'calc-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'calc-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'calc-cmd', + score: 0.028955796305270766 + } + ], + tabularx: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'tabularx-cmd', + score: 0.03789745970461662 + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'tabularx-cmd', + score: 0.0008038857295393196 + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'tabularx-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'tabularx-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'tabularx-cmd', + score: 0.0005861357565780464 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabularx-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'tabularx-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabularx-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'tabularx-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabularx-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'tabularx-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tabularx-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'tabularx-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'tabularx-cmd', + score: 0.018615449342361392 + } + ], + algorithm: [ + { + caption: '\\listalgorithmname', + snippet: '\\listalgorithmname', + meta: 'algorithm-cmd', + score: 0.00022490402516652368 + }, + { + caption: '\\listofalgorithms', + snippet: '\\listofalgorithms', + meta: 'algorithm-cmd', + score: 0.0012576983422794912 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithm-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithm-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithm-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithm-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithm-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0009837365348002915 + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0005815474978918903 + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'algorithm-cmd', + score: 0.0008866338267686714 + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'algorithm-cmd', + score: 0.0015470917047414941 + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0011934321931750752 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algorithm-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'algorithm-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'algorithm-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'algorithm-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'algorithm-cmd', + score: 0.0012745874472536625 + } + ], + biblatex: [ + { + caption: '\\textcite{}', + snippet: '\\textcite{$1}', + meta: 'biblatex-cmd', + score: 0.0071363824748767206 + }, + { + caption: '\\iffieldundef{}{}{}', + snippet: '\\iffieldundef{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5 + }, + { + caption: '\\list{}{}', + snippet: '\\list{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00046570666700199663 + }, + { + caption: '\\list{}', + snippet: '\\list{$1}', + meta: 'biblatex-cmd', + score: 0.00046570666700199663 + }, + { + caption: '\\list', + snippet: '\\list', + meta: 'biblatex-cmd', + score: 0.00046570666700199663 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'biblatex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'biblatex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\printbibliography', + snippet: '\\printbibliography', + meta: 'biblatex-cmd', + score: 0.028923378512954446 + }, + { + caption: '\\printbibliography[]', + snippet: '\\printbibliography[$1]', + meta: 'biblatex-cmd', + score: 0.028923378512954446 + }, + { + caption: '\\keyword{}', + snippet: '\\keyword{$1}', + meta: 'biblatex-cmd', + score: 0.0056978719547823445 + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'biblatex-cmd', + score: 0.04990693820960752 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\mkbibquote{}', + snippet: '\\mkbibquote{$1}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5 + }, + { + caption: '\\addabbrvspace', + snippet: '\\addabbrvspace', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5 + }, + { + caption: '\\AtEveryBibitem{}', + snippet: '\\AtEveryBibitem{$1}', + meta: 'biblatex-cmd', + score: 0.0006862523808353773 + }, + { + caption: '\\mkbibemph{}', + snippet: '\\mkbibemph{$1}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5 + }, + { + caption: '\\DeclareFieldFormat{}{}', + snippet: '\\DeclareFieldFormat{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00028207109055618685 + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'biblatex-cmd', + score: 0.2659628337907604 + }, + { + caption: '\\enquote{}', + snippet: '\\enquote{$1}', + meta: 'biblatex-cmd', + score: 0.0077432730806830915 + }, + { + caption: '\\bibopenbracket', + snippet: '\\bibopenbracket', + meta: 'biblatex-cmd', + score: 0.0005125772067631753 + }, + { + caption: '\\newbibmacro{}[]{}', + snippet: '\\newbibmacro{$1}[$2]{$3}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5 + }, + { + caption: '\\addbibresource{}', + snippet: '\\addbibresource{$1}', + meta: 'biblatex-cmd', + score: 0.033545778388159704 + }, + { + caption: '\\defbibheading{}{}', + snippet: '\\defbibheading{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00013423526504458629 + }, + { + caption: '\\DeclareNameAlias{}{}', + snippet: '\\DeclareNameAlias{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.0003596306478652252 + }, + { + caption: '\\bibcloseparen', + snippet: '\\bibcloseparen', + meta: 'biblatex-cmd', + score: 0.0005125772067631753 + }, + { + caption: '\\renewbibmacro{}{}', + snippet: '\\renewbibmacro{$1}{$2}', + meta: 'biblatex-cmd', + score: 9.70299207241043e-5 + }, + { + caption: '\\bibclosebracket', + snippet: '\\bibclosebracket', + meta: 'biblatex-cmd', + score: 0.0005125772067631753 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'biblatex-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'biblatex-cmd', + score: 3.800886892251021 + }, + { + caption: '\\parentext', + snippet: '\\parentext', + meta: 'biblatex-cmd', + score: 0.0005125772067631753 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'biblatex-cmd', + score: 2.341195220791228 + }, + { + caption: '\\addspace', + snippet: '\\addspace', + meta: 'biblatex-cmd', + score: 0.0002657609533376918 + }, + { + caption: '\\ifentrytype{}{}{}', + snippet: '\\ifentrytype{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 8.342875497183237e-5 + }, + { + caption: '\\addslash', + snippet: '\\addslash', + meta: 'biblatex-cmd', + score: 0.0002657609533376918 + }, + { + caption: '\\DefineBibliographyStrings{}{}', + snippet: '\\DefineBibliographyStrings{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.001537977148659816 + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'biblatex-cmd', + score: 3.0952612541683835 + }, + { + caption: '\\newblockpunct', + snippet: '\\newblockpunct', + meta: 'biblatex-cmd', + score: 0.0001328804766688459 + }, + { + caption: '\\defbibfilter{}{}', + snippet: '\\defbibfilter{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.0005203319717980072 + }, + { + caption: '\\parencite{}', + snippet: '\\parencite{$1}', + meta: 'biblatex-cmd', + score: 0.0447747090014577 + }, + { + caption: '\\parencite[]{}', + snippet: '\\parencite[$1]{$2}', + meta: 'biblatex-cmd', + score: 0.0447747090014577 + }, + { + caption: '\\midsentence', + snippet: '\\midsentence', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'biblatex-cmd', + score: 0.0004995635515943437 + }, + { + caption: '\\DeclareSourcemap{}', + snippet: '\\DeclareSourcemap{$1}', + meta: 'biblatex-cmd', + score: 0.0005203319717980072 + }, + { + caption: '\\AtBeginBibliography{}', + snippet: '\\AtBeginBibliography{$1}', + meta: 'biblatex-cmd', + score: 0.0004668773504581073 + }, + { + caption: '\\AtEveryCite{}', + snippet: '\\AtEveryCite{$1}', + meta: 'biblatex-cmd', + score: 0.0005125772067631753 + }, + { + caption: '\\DeclareLanguageMapping{}{}', + snippet: '\\DeclareLanguageMapping{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.000703956971675325 + }, + { + caption: '\\addtocategory{}{}', + snippet: '\\addtocategory{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.008238589553468446 + }, + { + caption: '\\DeclareBibliographyCategory{}', + snippet: '\\DeclareBibliographyCategory{$1}', + meta: 'biblatex-cmd', + score: 0.0010298236941835557 + }, + { + caption: '\\break', + snippet: '\\break', + meta: 'biblatex-cmd', + score: 0.016352452390960115 + }, + { + caption: '\\break{}', + snippet: '\\break{$1}', + meta: 'biblatex-cmd', + score: 0.016352452390960115 + }, + { + caption: '\\break{}{}', + snippet: '\\break{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.016352452390960115 + }, + { + caption: '\\bibopenparen', + snippet: '\\bibopenparen', + meta: 'biblatex-cmd', + score: 0.0005125772067631753 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\name{}{}', + snippet: '\\name{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.1236289144754329 + }, + { + caption: '\\name', + snippet: '\\name', + meta: 'biblatex-cmd', + score: 0.1236289144754329 + }, + { + caption: '\\name{}', + snippet: '\\name{$1}', + meta: 'biblatex-cmd', + score: 0.1236289144754329 + }, + { + caption: '\\ExecuteBibliographyOptions{}', + snippet: '\\ExecuteBibliographyOptions{$1}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5 + }, + { + caption: '\\usebibmacro{}{}', + snippet: '\\usebibmacro{$1}{$2}', + meta: 'biblatex-cmd', + score: 9.682965195065755e-5 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'biblatex-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'biblatex-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'biblatex-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'biblatex-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'biblatex-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'biblatex-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'biblatex-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'biblatex-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'biblatex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'biblatex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'biblatex-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'biblatex-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'biblatex-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'biblatex-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'biblatex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'biblatex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'biblatex-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'biblatex-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'biblatex-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'biblatex-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'biblatex-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'biblatex-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'biblatex-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157 + } + ], + microtype: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'microtype-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'microtype-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'microtype-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\lsstyle', + snippet: '\\lsstyle', + meta: 'microtype-cmd', + score: 0.0023367519914345774 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'microtype-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\DisableLigatures[]{}', + snippet: '\\DisableLigatures[$1]{$2}', + meta: 'microtype-cmd', + score: 0.0009805246614299932 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'microtype-cmd', + score: 0.00037306820619479756 + } + ], + etoolbox: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'etoolbox-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'etoolbox-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'etoolbox-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'etoolbox-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'etoolbox-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'etoolbox-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'etoolbox-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'etoolbox-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'etoolbox-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'etoolbox-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'etoolbox-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'etoolbox-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'etoolbox-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etoolbox-cmd', + score: 0.008565354665444157 + } + ], + longtable: [ + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'longtable-cmd', + score: 0.0023853501147448834 + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'longtable-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'longtable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'longtable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'longtable-cmd', + score: 9.952664522415981e-5 + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'longtable-cmd', + score: 0.0016148498709822416 + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'longtable-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'longtable-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'longtable-cmd', + score: 0.0029238994233674776 + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'longtable-cmd', + score: 0.0313525090421608 + } + ], + mathtools: [ + { + caption: '\\xleftrightarrow[][]{}', + snippet: '\\xleftrightarrow[$1][$2]{$3}', + meta: 'mathtools-cmd', + score: 4.015559489911509e-5 + }, + { + caption: '\\vcentcolon', + snippet: '\\vcentcolon', + meta: 'mathtools-cmd', + score: 0.00021361943526711615 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'mathtools-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\coloneqq', + snippet: '\\coloneqq', + meta: 'mathtools-cmd', + score: 0.0014407293323958122 + }, + { + caption: '\\mathclap{}', + snippet: '\\mathclap{$1}', + meta: 'mathtools-cmd', + score: 7.84378567451772e-5 + }, + { + caption: '\\adjustlimits', + snippet: '\\adjustlimits', + meta: 'mathtools-cmd', + score: 0.0005307066890271085 + }, + { + caption: '\\MoveEqLeft', + snippet: '\\MoveEqLeft', + meta: 'mathtools-cmd', + score: 5.343949980628182e-5 + }, + { + caption: '\\mathrlap{}', + snippet: '\\mathrlap{$1}', + meta: 'mathtools-cmd', + score: 0.0003112817211637952 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'mathtools-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\xhookrightarrow{}', + snippet: '\\xhookrightarrow{$1}', + meta: 'mathtools-cmd', + score: 5.444260823474129e-5 + }, + { + caption: '\\DeclarePairedDelimiter{}{}{}', + snippet: '\\DeclarePairedDelimiter{$1}{$2}{$3}', + meta: 'mathtools-cmd', + score: 0.0033916678416372487 + }, + { + caption: '\\DeclarePairedDelimiter', + snippet: '\\DeclarePairedDelimiter', + meta: 'mathtools-cmd', + score: 0.0033916678416372487 + }, + { + caption: '\\prescript{}{}{}', + snippet: '\\prescript{$1}{$2}{$3}', + meta: 'mathtools-cmd', + score: 8.833369785705982e-6 + }, + { + caption: '\\underbrace{}', + snippet: '\\underbrace{$1}', + meta: 'mathtools-cmd', + score: 0.010373780436850907 + }, + { + caption: '\\mathllap{}', + snippet: '\\mathllap{$1}', + meta: 'mathtools-cmd', + score: 3.140504277052775e-5 + }, + { + caption: '\\overbrace{}', + snippet: '\\overbrace{$1}', + meta: 'mathtools-cmd', + score: 0.0006045704778718376 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'mathtools-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'mathtools-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'mathtools-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'mathtools-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mathtools-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mathtools-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mathtools-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'mathtools-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mathtools-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mathtools-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'mathtools-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathtools-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'mathtools-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'mathtools-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'mathtools-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'mathtools-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'mathtools-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'mathtools-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'mathtools-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'mathtools-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'mathtools-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'mathtools-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'mathtools-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'mathtools-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'mathtools-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'mathtools-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'mathtools-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'mathtools-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'mathtools-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'mathtools-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'mathtools-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'mathtools-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'mathtools-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'mathtools-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'mathtools-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'mathtools-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'mathtools-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'mathtools-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'mathtools-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'mathtools-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'mathtools-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'mathtools-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'mathtools-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'mathtools-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'mathtools-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'mathtools-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'mathtools-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'mathtools-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'mathtools-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'mathtools-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'mathtools-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'mathtools-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'mathtools-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mathtools-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'mathtools-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'mathtools-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'mathtools-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'mathtools-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'mathtools-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'mathtools-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'mathtools-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'mathtools-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'mathtools-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'mathtools-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'mathtools-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'mathtools-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'mathtools-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'mathtools-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'mathtools-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'mathtools-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'mathtools-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'mathtools-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'mathtools-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'mathtools-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'mathtools-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'mathtools-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'mathtools-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'mathtools-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'mathtools-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'mathtools-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'mathtools-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'mathtools-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'mathtools-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'mathtools-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'mathtools-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mathtools-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'mathtools-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'mathtools-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'mathtools-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'mathtools-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'mathtools-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'mathtools-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'mathtools-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'mathtools-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'mathtools-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'mathtools-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'mathtools-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'mathtools-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'mathtools-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'mathtools-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'mathtools-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'mathtools-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'mathtools-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'mathtools-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'mathtools-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'mathtools-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'mathtools-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'mathtools-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'mathtools-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'mathtools-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'mathtools-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'mathtools-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'mathtools-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'mathtools-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'mathtools-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'mathtools-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'mathtools-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'mathtools-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'mathtools-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'mathtools-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'mathtools-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'mathtools-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'mathtools-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'mathtools-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'mathtools-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'mathtools-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'mathtools-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'mathtools-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'mathtools-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'mathtools-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'mathtools-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'mathtools-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'mathtools-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'mathtools-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'mathtools-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'mathtools-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'mathtools-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'mathtools-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'mathtools-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'mathtools-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'mathtools-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'mathtools-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'mathtools-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mathtools-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'mathtools-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mathtools-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathtools-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'mathtools-cmd', + score: 0.0063276692758974925 + } + ], + verbatim: [ + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'verbatim-cmd', + score: 0.0022216421267780076 + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'verbatim-cmd', + score: 0.0072203369120285256 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'verbatim-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'verbatim-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'verbatim-cmd', + score: 0.413853376001159 + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'verbatim-cmd', + score: 0.0024547099784948665 + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'verbatim-cmd', + score: 0.0024547099784948665 + } + ], + wrapfig: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'wrapfig-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'wrapfig-cmd', + score: 0.413853376001159 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wrapfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wrapfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\wrapfigure{}{}', + snippet: '\\wrapfigure{$1}{$2}', + meta: 'wrapfig-cmd', + score: 0.0003295435821387379 + } + ], + epsfig: [ + { + caption: '\\epsfbox{}', + snippet: '\\epsfbox{$1}', + meta: 'epsfig-cmd', + score: 0.00013712781345832882 + }, + { + caption: '\\psfig{}', + snippet: '\\psfig{$1}', + meta: 'epsfig-cmd', + score: 0.0017552046452897515 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'epsfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'epsfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'epsfig-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'epsfig-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'epsfig-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'epsfig-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epsfig-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'epsfig-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsfig-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'epsfig-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'epsfig-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsfig-cmd', + score: 0.008565354665444157 + } + ], + cite: [ + { + caption: '\\citeonline{}', + snippet: '\\citeonline{$1}', + meta: 'cite-cmd', + score: 0.014277840409455324 + }, + { + caption: '\\citenum{}', + snippet: '\\citenum{$1}', + meta: 'cite-cmd', + score: 0.0027420903627423383 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'cite-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'cite-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'cite-cmd', + score: 0.04990693820960752 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'cite-cmd', + score: 2.341195220791228 + } + ], + lipsum: [ + { + caption: '\\setlipsumdefault{}', + snippet: '\\setlipsumdefault{$1}', + meta: 'lipsum-cmd', + score: 0.00024112945034541791 + }, + { + caption: '\\lipsum[]', + snippet: '\\lipsum[$1]', + meta: 'lipsum-cmd', + score: 0.0300787181624191 + } + ], + algpseudocode: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algpseudocode-cmd', + score: 0.0019861803661869416 + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algpseudocode-cmd', + score: 0.008622777195102994 + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algpseudocode-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algpseudocode-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algpseudocode-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algpseudocode-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algpseudocode-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algpseudocode-cmd', + score: 0.005178604573219454 + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algpseudocode-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algpseudocode-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algpseudocode-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algpseudocode-cmd', + score: 0.0007916858220314837 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algpseudocode-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algpseudocode-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algpseudocode-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algpseudocode-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algpseudocode-cmd', + score: 0.0005463612015579842 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algpseudocode-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algpseudocode-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algpseudocode-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algpseudocode-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algpseudocode-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algpseudocode-cmd', + score: 0.0018957469739775527 + } + ], + textpos: [ + { + caption: '\\textblockorigin{}{}', + snippet: '\\textblockorigin{$1}{$2}', + meta: 'textpos-cmd', + score: 0.016306266556901577 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'textpos-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'textpos-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'textpos-cmd', + score: 0.00037306820619479756 + } + ], + subfig: [ + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subfig-cmd', + score: 0.007192033516871399 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'subfig-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\subfloat[]{}', + snippet: '\\subfloat[$1]{$2}', + meta: 'subfig-cmd', + score: 0.0286920437310672 + }, + { + caption: '\\subfloat{}', + snippet: '\\subfloat{$1}', + meta: 'subfig-cmd', + score: 0.0286920437310672 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'subfig-cmd', + score: 0.00037306820619479756 + } + ], + enumerate: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'enumerate-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\makelabel', + snippet: '\\makelabel', + meta: 'enumerate-cmd', + score: 5.739925426740175e-5 + }, + { + caption: '\\makelabel{}', + snippet: '\\makelabel{$1}', + meta: 'enumerate-cmd', + score: 5.739925426740175e-5 + }, + { + caption: '\\makelabel[]{}', + snippet: '\\makelabel[$1]{$2}', + meta: 'enumerate-cmd', + score: 5.739925426740175e-5 + } + ], + pdfpages: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfpages-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'pdfpages-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pdfpages-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\includepdf[]{}', + snippet: '\\includepdf[$1]{$2}', + meta: 'pdfpages-cmd', + score: 0.023931732745590156 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'pdfpages-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'pdfpages-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'pdfpages-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdfpages-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'pdfpages-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'pdfpages-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfpages-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'pdfpages-cmd', + score: 0.000325977535138643 + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'pdfpages-cmd', + score: 0.0008957666085644653 + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'pdfpages-cmd', + score: 0.0003608141410278152 + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'pdfpages-cmd', + score: 0.0007216282820556304 + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'pdfpages-cmd', + score: 0.0017658629469099734 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pdfpages-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pdfpages-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pdfpages-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pdfpages-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pdfpages-cmd', + score: 0.0018957469739775527 + } + ], + epstopdf: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\AppendGraphicsExtensions{}', + snippet: '\\AppendGraphicsExtensions{$1}', + meta: 'epstopdf-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'epstopdf-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'epstopdf-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'epstopdf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'epstopdf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\epstopdfsetup{}', + snippet: '\\epstopdfsetup{$1}', + meta: 'epstopdf-cmd', + score: 0.0009941134326203623 + }, + { + caption: '\\epstopdfDeclareGraphicsRule{}{}{}{}', + snippet: '\\epstopdfDeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'epstopdf-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\OutputFile', + snippet: '\\OutputFile', + meta: 'epstopdf-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epstopdf-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epstopdf-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157 + } + ], + lmodern: [ + { + caption: '\\rmdefault', + snippet: '\\rmdefault', + meta: 'lmodern-cmd', + score: 0.0012870877747432935 + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'lmodern-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'lmodern-cmd', + score: 0.008427383388519996 + } + ], + pifont: [ + { + caption: '\\ding{}', + snippet: '\\ding{$1}', + meta: 'pifont-cmd', + score: 0.009992300665793867 + } + ], + ragged2e: [ + { + caption: '\\justifying', + snippet: '\\justifying', + meta: 'ragged2e-cmd', + score: 0.010373702256548788 + }, + { + caption: '\\justifying{}', + snippet: '\\justifying{$1}', + meta: 'ragged2e-cmd', + score: 0.010373702256548788 + }, + { + caption: '\\RaggedRight', + snippet: '\\RaggedRight', + meta: 'ragged2e-cmd', + score: 0.001021021782267457 + }, + { + caption: '\\Centering', + snippet: '\\Centering', + meta: 'ragged2e-cmd', + score: 0.00037395241488843035 + }, + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'ragged2e-cmd', + score: 0.04598628699063736 + } + ], + rotating: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotating-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'rotating-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'rotating-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'rotating-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'rotating-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'rotating-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'rotating-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'rotating-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'rotating-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rotating-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'rotating-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotating-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'rotating-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotating-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotating-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotating-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'rotating-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'rotating-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'rotating-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'rotating-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'rotating-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'rotating-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'rotating-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotating-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotating-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotating-cmd', + score: 0.004719094298848707 + } + ], + xltxtra: [ + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'xltxtra-cmd', + score: 0.058405875394131175 + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'xltxtra-cmd', + score: 0.05216393882408519 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xltxtra-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'xltxtra-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xltxtra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xltxtra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xltxtra-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xltxtra-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xltxtra-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xltxtra-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xltxtra-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xltxtra-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xltxtra-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xltxtra-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xltxtra-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\XeTeX', + snippet: '\\XeTeX', + meta: 'xltxtra-cmd', + score: 0.0010635559050357936 + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'xltxtra-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'xltxtra-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'xltxtra-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'xltxtra-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\XeLaTeX', + snippet: '\\XeLaTeX', + meta: 'xltxtra-cmd', + score: 0.002009786035379175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xltxtra-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xltxtra-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xltxtra-cmd', + score: 0.2864294797053033 + } + ], + marvosym: [ + { + caption: '\\Mundus', + snippet: '\\Mundus', + meta: 'marvosym-cmd', + score: 0.0006349134235582933 + }, + { + caption: '\\Telefon', + snippet: '\\Telefon', + meta: 'marvosym-cmd', + score: 0.0003618274070138519 + }, + { + caption: '\\Letter', + snippet: '\\Letter', + meta: 'marvosym-cmd', + score: 0.0012281130571092198 + }, + { + caption: '\\Mobilefone', + snippet: '\\Mobilefone', + meta: 'marvosym-cmd', + score: 0.0005432037068220953 + } + ], + dcolumn: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'dcolumn-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'dcolumn-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'dcolumn-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'dcolumn-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'dcolumn-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dcolumn-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'dcolumn-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'dcolumn-cmd', + score: 0.018615449342361392 + } + ], + xspace: [ + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'xspace-cmd', + score: 0.07560370351316588 + } + ], + xunicode: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xunicode-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xunicode-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xunicode-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xunicode-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xunicode-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xunicode-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xunicode-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xunicode-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xunicode-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xunicode-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xunicode-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xunicode-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xunicode-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xunicode-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xunicode-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xunicode-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xunicode-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xunicode-cmd', + score: 0.008565354665444157 + } + ], + csquotes: [ + { + caption: '\\mkcitation', + snippet: '\\mkcitation', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\DeclareQuoteAlias{}{}', + snippet: '\\DeclareQuoteAlias{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.0004906235524176374 + }, + { + caption: '\\quote{}', + snippet: '\\quote{$1}', + meta: 'csquotes-cmd', + score: 0.030690393112264815 + }, + { + caption: '\\quote', + snippet: '\\quote', + meta: 'csquotes-cmd', + score: 0.030690393112264815 + }, + { + caption: '\\setquotestyle[]{}', + snippet: '\\setquotestyle[$1]{$2}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\blockquote{}', + snippet: '\\blockquote{$1}', + meta: 'csquotes-cmd', + score: 0.00023365626458085812 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'csquotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'csquotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\mkbegdispquote', + snippet: '\\mkbegdispquote', + meta: 'csquotes-cmd', + score: 4.203362017075738e-5 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'csquotes-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\break', + snippet: '\\break', + meta: 'csquotes-cmd', + score: 0.016352452390960115 + }, + { + caption: '\\break{}', + snippet: '\\break{$1}', + meta: 'csquotes-cmd', + score: 0.016352452390960115 + }, + { + caption: '\\break{}{}', + snippet: '\\break{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.016352452390960115 + }, + { + caption: '\\ifpunctmark{}', + snippet: '\\ifpunctmark{$1}', + meta: 'csquotes-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\endquote', + snippet: '\\endquote', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'csquotes-cmd', + score: 0.413853376001159 + }, + { + caption: '\\DeclareQuoteStyle[]{}', + snippet: '\\DeclareQuoteStyle[$1]{$2}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\SetBlockEnvironment{}', + snippet: '\\SetBlockEnvironment{$1}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'csquotes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\MakeOuterQuote{}', + snippet: '\\MakeOuterQuote{$1}', + meta: 'csquotes-cmd', + score: 0.0019170811203505262 + }, + { + caption: '\\enquote{}', + snippet: '\\enquote{$1}', + meta: 'csquotes-cmd', + score: 0.0077432730806830915 + }, + { + caption: '\\SetCiteCommand{}', + snippet: '\\SetCiteCommand{$1}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'csquotes-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'csquotes-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'csquotes-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'csquotes-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'csquotes-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'csquotes-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'csquotes-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'csquotes-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'csquotes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'csquotes-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'csquotes-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'csquotes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.00037306820619479756 + } + ], + xparse: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xparse-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xparse-cmd', + score: 0.2864294797053033 + } + ], + soul: [ + { + caption: '\\DeclareRobustCommand{}{}', + snippet: '\\DeclareRobustCommand{$1}{$2}', + meta: 'soul-cmd', + score: 0.0010373158471650705 + }, + { + caption: '\\DeclareRobustCommand{}[]{}', + snippet: '\\DeclareRobustCommand{$1}[$2]{$3}', + meta: 'soul-cmd', + score: 0.0010373158471650705 + }, + { + caption: '\\sethlcolor{}', + snippet: '\\sethlcolor{$1}', + meta: 'soul-cmd', + score: 0.01970230898277056 + }, + { + caption: '\\st', + snippet: '\\st', + meta: 'soul-cmd', + score: 0.004652662833362787 + }, + { + caption: '\\st{}', + snippet: '\\st{$1}', + meta: 'soul-cmd', + score: 0.004652662833362787 + }, + { + caption: '\\def', + snippet: '\\def', + meta: 'soul-cmd', + score: 0.21357759092476175 + }, + { + caption: '\\hl{}', + snippet: '\\hl{$1}', + meta: 'soul-cmd', + score: 0.03421486301062431 + }, + { + caption: '\\sodef', + snippet: '\\sodef', + meta: 'soul-cmd', + score: 0.0017045357696831268 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'soul-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\so', + snippet: '\\so', + meta: 'soul-cmd', + score: 0.004308800134587786 + }, + { + caption: '\\so{}', + snippet: '\\so{$1}', + meta: 'soul-cmd', + score: 0.004308800134587786 + } + ], + comment: [ + { + caption: '\\specialcomment{}{}{}', + snippet: '\\specialcomment{$1}{$2}{$3}', + meta: 'comment-cmd', + score: 9.120209837787948e-5 + }, + { + caption: '\\includecomment{}', + snippet: '\\includecomment{$1}', + meta: 'comment-cmd', + score: 8.21804444236254e-5 + } + ], + algorithm2e: [ + { + caption: '\\FuncSty{}', + snippet: '\\FuncSty{$1}', + meta: 'algorithm2e-cmd', + score: 7.576875738934807e-5 + }, + { + caption: '\\algorithmautorefname', + snippet: '\\algorithmautorefname', + meta: 'algorithm2e-cmd', + score: 2.0085955839419213e-5 + }, + { + caption: '\\SetAlgoNoLine', + snippet: '\\SetAlgoNoLine', + meta: 'algorithm2e-cmd', + score: 0.00015722499147840545 + }, + { + caption: '\\Indp', + snippet: '\\Indp', + meta: 'algorithm2e-cmd', + score: 6.068942580823901e-5 + }, + { + caption: '\\AlCapFnt', + snippet: '\\AlCapFnt', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5 + }, + { + caption: '\\LinesNumbered', + snippet: '\\LinesNumbered', + meta: 'algorithm2e-cmd', + score: 0.000162125616653719 + }, + { + caption: '\\SetAlFnt{}', + snippet: '\\SetAlFnt{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024446198714390757 + }, + { + caption: '\\SetKw{}{}', + snippet: '\\SetKw{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 9.292434841280213e-5 + }, + { + caption: '\\RestyleAlgo{}', + snippet: '\\RestyleAlgo{$1}', + meta: 'algorithm2e-cmd', + score: 0.00019243311960945823 + }, + { + caption: '\\listofalgorithms', + snippet: '\\listofalgorithms', + meta: 'algorithm2e-cmd', + score: 0.0012576983422794912 + }, + { + caption: '\\IncMargin{}', + snippet: '\\IncMargin{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063 + }, + { + caption: '\\BlankLine', + snippet: '\\BlankLine', + meta: 'algorithm2e-cmd', + score: 0.005049617303688214 + }, + { + caption: '\\SetCommentSty{}', + snippet: '\\SetCommentSty{$1}', + meta: 'algorithm2e-cmd', + score: 0.0001778112853266571 + }, + { + caption: '\\SetAlgoNoEnd', + snippet: '\\SetAlgoNoEnd', + meta: 'algorithm2e-cmd', + score: 0.00015722499147840545 + }, + { + caption: '\\theAlgoLine{}', + snippet: '\\theAlgoLine{$1}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5 + }, + { + caption: '\\SetKwBlock{}{}{}', + snippet: '\\SetKwBlock{$1}{$2}{$3}', + meta: 'algorithm2e-cmd', + score: 0.000981463850523159 + }, + { + caption: '\\SetKwBlock{}{}', + snippet: '\\SetKwBlock{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.000981463850523159 + }, + { + caption: '\\AlCapNameFnt', + snippet: '\\AlCapNameFnt', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5 + }, + { + caption: '\\SetAlgoSkip{}', + snippet: '\\SetAlgoSkip{$1}', + meta: 'algorithm2e-cmd', + score: 0.00017454032258926576 + }, + { + caption: '\\SetKwFunction{}{}', + snippet: '\\SetKwFunction{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.0015332307832994817 + }, + { + caption: '\\nllabel{}', + snippet: '\\nllabel{$1}', + meta: 'algorithm2e-cmd', + score: 0.0001844460347791443 + }, + { + caption: '\\SetAlgoInsideSkip{}', + snippet: '\\SetAlgoInsideSkip{$1}', + meta: 'algorithm2e-cmd', + score: 4.5812360816321294e-5 + }, + { + caption: '\\DataSty{}', + snippet: '\\DataSty{$1}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5 + }, + { + caption: '\\SetKwInOut{}{}', + snippet: '\\SetKwInOut{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.0017021978326807814 + }, + { + caption: '\\SetAlCapFnt{}', + snippet: '\\SetAlCapFnt{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063 + }, + { + caption: '\\CommentSty{}', + snippet: '\\CommentSty{$1}', + meta: 'algorithm2e-cmd', + score: 0.0001111448631633176 + }, + { + caption: '\\SetAlCapHSkip{}', + snippet: '\\SetAlCapHSkip{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063 + }, + { + caption: '\\renewcommand{}{}', + snippet: '\\renewcommand{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.3267437011085663 + }, + { + caption: '\\renewcommand', + snippet: '\\renewcommand', + meta: 'algorithm2e-cmd', + score: 0.3267437011085663 + }, + { + caption: '\\algorithmcfname', + snippet: '\\algorithmcfname', + meta: 'algorithm2e-cmd', + score: 0.0024445413067013134 + }, + { + caption: '\\SetKwIF{}{}{}{}{}{}{}{}', + snippet: '\\SetKwIF{$1}{$2}{$3}{$4}{$5}{$6}{$7}{$8}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5 + }, + { + caption: '\\SetAlgoCaptionSeparator{}', + snippet: '\\SetAlgoCaptionSeparator{$1}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5 + }, + { + caption: '\\AlCapSty{}', + snippet: '\\AlCapSty{$1}', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5 + }, + { + caption: '\\ArgSty{}', + snippet: '\\ArgSty{$1}', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5 + }, + { + caption: '\\AlCapNameSty{}', + snippet: '\\AlCapNameSty{$1}', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5 + }, + { + caption: '\\SetKwData{}{}', + snippet: '\\SetKwData{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.00235652682860263 + }, + { + caption: '\\listalgorithmcfname', + snippet: '\\listalgorithmcfname', + meta: 'algorithm2e-cmd', + score: 1.5075186740106946e-5 + }, + { + caption: '\\Indm', + snippet: '\\Indm', + meta: 'algorithm2e-cmd', + score: 6.068942580823901e-5 + }, + { + caption: '\\SetAlCapNameFnt{}', + snippet: '\\SetAlCapNameFnt{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063 + }, + { + caption: '\\DontPrintSemicolon', + snippet: '\\DontPrintSemicolon', + meta: 'algorithm2e-cmd', + score: 0.001062087490197768 + }, + { + caption: '\\SetAlgoLined', + snippet: '\\SetAlgoLined', + meta: 'algorithm2e-cmd', + score: 0.0017151361342403852 + }, + { + caption: '\\SetAlCapSkip{}', + snippet: '\\SetAlCapSkip{$1}', + meta: 'algorithm2e-cmd', + score: 0.0006213942502400296 + }, + { + caption: '\\LinesNotNumbered', + snippet: '\\LinesNotNumbered', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5 + }, + { + caption: '\\SetKwProg{}{}{}{}', + snippet: '\\SetKwProg{$1}{$2}{$3}{$4}', + meta: 'algorithm2e-cmd', + score: 0.0008518783278391971 + }, + { + caption: '\\SetAlgoVlined', + snippet: '\\SetAlgoVlined', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5 + }, + { + caption: '\\SetKwRepeat{}{}{}', + snippet: '\\SetKwRepeat{$1}{$2}{$3}', + meta: 'algorithm2e-cmd', + score: 6.110202388233705e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algorithm2e-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'algorithm2e-cmd', + score: 0.422097569591803 + }, + { + caption: '\\SetKwFor{}{}{}{}', + snippet: '\\SetKwFor{$1}{$2}{$3}{$4}', + meta: 'algorithm2e-cmd', + score: 0.00010699539949594301 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithm2e-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithm2e-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithm2e-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithm2e-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithm2e-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'algorithm2e-cmd', + score: 0.07560370351316588 + } + ], + tocbibind: [ + { + caption: '\\contentsname', + snippet: '\\contentsname', + meta: 'tocbibind-cmd', + score: 0.010205180337548728 + }, + { + caption: '\\contentsname{}', + snippet: '\\contentsname{$1}', + meta: 'tocbibind-cmd', + score: 0.010205180337548728 + }, + { + caption: '\\tocchapter', + snippet: '\\tocchapter', + meta: 'tocbibind-cmd', + score: 0.00016023188758771694 + }, + { + caption: '\\indexname', + snippet: '\\indexname', + meta: 'tocbibind-cmd', + score: 0.0007544109314450072 + }, + { + caption: '\\listoffigures', + snippet: '\\listoffigures', + meta: 'tocbibind-cmd', + score: 0.03447318897846567 + }, + { + caption: '\\tocfile{}{}', + snippet: '\\tocfile{$1}{$2}', + meta: 'tocbibind-cmd', + score: 0.00016023188758771694 + }, + { + caption: '\\tocbibname', + snippet: '\\tocbibname', + meta: 'tocbibind-cmd', + score: 0.0020762574479507175 + }, + { + caption: '\\settocbibname{}', + snippet: '\\settocbibname{$1}', + meta: 'tocbibind-cmd', + score: 0.00010668677119599426 + }, + { + caption: '\\listoftables', + snippet: '\\listoftables', + meta: 'tocbibind-cmd', + score: 0.02104656820469027 + }, + { + caption: '\\tableofcontents', + snippet: '\\tableofcontents', + meta: 'tocbibind-cmd', + score: 0.13360595130994957 + }, + { + caption: '\\listfigurename', + snippet: '\\listfigurename', + meta: 'tocbibind-cmd', + score: 0.0034407237779350256 + } + ], + pgfplots: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplots-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplots-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplots-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfplots-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfplots-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfplots-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplots-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfplots-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplots-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfplots-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfplots-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfplots-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplots-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplots-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfplots-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplots-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfplots-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplots-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfplots-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfplots-cmd', + score: 0.2864294797053033 + } + ], + lastpage: [ + { + caption: '\\string', + snippet: '\\string', + meta: 'lastpage-cmd', + score: 0.001042697111754002 + } + ], + graphics: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'graphics-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'graphics-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'graphics-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'graphics-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'graphics-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'graphics-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'graphics-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'graphics-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'graphics-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphics-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'graphics-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphics-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphics-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphics-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'graphics-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphics-cmd', + score: 0.008565354665444157 + } + ], + algorithmic: [ + { + caption: '\\REPEAT', + snippet: '\\REPEAT', + meta: 'algorithmic-cmd', + score: 0.0004816110638193742 + }, + { + caption: '\\ENDIF', + snippet: '\\ENDIF', + meta: 'algorithmic-cmd', + score: 0.003585213685098552 + }, + { + caption: '\\algorithmicwhile', + snippet: '\\algorithmicwhile', + meta: 'algorithmic-cmd', + score: 0.0005769483780443573 + }, + { + caption: '\\algorithmicwhile{}', + snippet: '\\algorithmicwhile{$1}', + meta: 'algorithmic-cmd', + score: 0.0005769483780443573 + }, + { + caption: '\\FOR{}', + snippet: '\\FOR{$1}', + meta: 'algorithmic-cmd', + score: 0.004074774218819945 + }, + { + caption: '\\algorithmicif', + snippet: '\\algorithmicif', + meta: 'algorithmic-cmd', + score: 0.00039654130753044966 + }, + { + caption: '\\algorithmicif{}', + snippet: '\\algorithmicif{$1}', + meta: 'algorithmic-cmd', + score: 0.00039654130753044966 + }, + { + caption: '\\ENDFOR', + snippet: '\\ENDFOR', + meta: 'algorithmic-cmd', + score: 0.004428141530092572 + }, + { + caption: '\\UNTIL', + snippet: '\\UNTIL', + meta: 'algorithmic-cmd', + score: 0.0004816110638193742 + }, + { + caption: '\\UNTIL{}', + snippet: '\\UNTIL{$1}', + meta: 'algorithmic-cmd', + score: 0.0004816110638193742 + }, + { + caption: '\\IF{}', + snippet: '\\IF{$1}', + meta: 'algorithmic-cmd', + score: 0.0036985887706967417 + }, + { + caption: '\\ENSURE', + snippet: '\\ENSURE', + meta: 'algorithmic-cmd', + score: 0.0013188761425395954 + }, + { + caption: '\\algorithmiccomment', + snippet: '\\algorithmiccomment', + meta: 'algorithmic-cmd', + score: 0.00021737766481978388 + }, + { + caption: '\\ENDWHILE', + snippet: '\\ENDWHILE', + meta: 'algorithmic-cmd', + score: 0.00047037943460091465 + }, + { + caption: '\\algorithmicend', + snippet: '\\algorithmicend', + meta: 'algorithmic-cmd', + score: 0.0011128218085672747 + }, + { + caption: '\\algorithmicend{}', + snippet: '\\algorithmicend{$1}', + meta: 'algorithmic-cmd', + score: 0.0011128218085672747 + }, + { + caption: '\\algorithmicrequire', + snippet: '\\algorithmicrequire', + meta: 'algorithmic-cmd', + score: 0.004751598472180266 + }, + { + caption: '\\algorithmicdo', + snippet: '\\algorithmicdo', + meta: 'algorithmic-cmd', + score: 0.0005655570358533174 + }, + { + caption: '\\algorithmicdo{}', + snippet: '\\algorithmicdo{$1}', + meta: 'algorithmic-cmd', + score: 0.0005655570358533174 + }, + { + caption: '\\algorithmicfor', + snippet: '\\algorithmicfor', + meta: 'algorithmic-cmd', + score: 0.0005681785898943757 + }, + { + caption: '\\algorithmicfor{}', + snippet: '\\algorithmicfor{$1}', + meta: 'algorithmic-cmd', + score: 0.0005681785898943757 + }, + { + caption: '\\RETURN', + snippet: '\\RETURN', + meta: 'algorithmic-cmd', + score: 0.0013054907995767408 + }, + { + caption: '\\algorithmicand', + snippet: '\\algorithmicand', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5 + }, + { + caption: '\\algsetup{}', + snippet: '\\algsetup{$1}', + meta: 'algorithmic-cmd', + score: 0.00012872796177294446 + }, + { + caption: '\\algorithmicreturn{}', + snippet: '\\algorithmicreturn{$1}', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368 + }, + { + caption: '\\algorithmicreturn', + snippet: '\\algorithmicreturn', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368 + }, + { + caption: '\\algorithmicforall{}', + snippet: '\\algorithmicforall{$1}', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368 + }, + { + caption: '\\algorithmicforall', + snippet: '\\algorithmicforall', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368 + }, + { + caption: '\\COMMENT', + snippet: '\\COMMENT', + meta: 'algorithmic-cmd', + score: 0.00025669572555354604 + }, + { + caption: '\\COMMENT{}', + snippet: '\\COMMENT{$1}', + meta: 'algorithmic-cmd', + score: 0.00025669572555354604 + }, + { + caption: '\\REQUIRE', + snippet: '\\REQUIRE', + meta: 'algorithmic-cmd', + score: 0.001870681168192269 + }, + { + caption: '\\algorithmicor', + snippet: '\\algorithmicor', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5 + }, + { + caption: '\\ELSE', + snippet: '\\ELSE', + meta: 'algorithmic-cmd', + score: 0.0007599864146830139 + }, + { + caption: '\\STATE', + snippet: '\\STATE', + meta: 'algorithmic-cmd', + score: 0.0266684860947573 + }, + { + caption: '\\WHILE{}', + snippet: '\\WHILE{$1}', + meta: 'algorithmic-cmd', + score: 0.00047037943460091465 + }, + { + caption: '\\ELSIF{}', + snippet: '\\ELSIF{$1}', + meta: 'algorithmic-cmd', + score: 0.0001991613148371481 + }, + { + caption: '\\FALSE', + snippet: '\\FALSE', + meta: 'algorithmic-cmd', + score: 3.34222699937868e-5 + }, + { + caption: '\\AND', + snippet: '\\AND', + meta: 'algorithmic-cmd', + score: 6.401730289932545e-5 + }, + { + caption: '\\algorithmicensure', + snippet: '\\algorithmicensure', + meta: 'algorithmic-cmd', + score: 0.003439482525198322 + }, + { + caption: '\\OR', + snippet: '\\OR', + meta: 'algorithmic-cmd', + score: 6.401730289932545e-5 + }, + { + caption: '\\algorithmicrepeat', + snippet: '\\algorithmicrepeat', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5 + }, + { + caption: '\\TRUE', + snippet: '\\TRUE', + meta: 'algorithmic-cmd', + score: 0.0001336890799751472 + }, + { + caption: '\\FORALL{}', + snippet: '\\FORALL{$1}', + meta: 'algorithmic-cmd', + score: 0.0003533673112726266 + }, + { + caption: '\\algorithmicthen{}', + snippet: '\\algorithmicthen{$1}', + meta: 'algorithmic-cmd', + score: 0.00032476571672371697 + }, + { + caption: '\\algorithmicthen', + snippet: '\\algorithmicthen', + meta: 'algorithmic-cmd', + score: 0.00032476571672371697 + }, + { + caption: '\\algorithmicuntil', + snippet: '\\algorithmicuntil', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithmic-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithmic-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithmic-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithmic-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithmic-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithmic-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'algorithmic-cmd', + score: 0.00037306820619479756 + } + ], + lineno: [ + { + caption: '\\pagewiselinenumbers', + snippet: '\\pagewiselinenumbers', + meta: 'lineno-cmd', + score: 0.00016870831850106035 + }, + { + caption: '\\linenomath', + snippet: '\\linenomath', + meta: 'lineno-cmd', + score: 1.4517338420208715e-5 + }, + { + caption: '\\linenumberfont{}', + snippet: '\\linenumberfont{$1}', + meta: 'lineno-cmd', + score: 0.0001811784338695797 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lineno-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lineno-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\endlinenomath', + snippet: '\\endlinenomath', + meta: 'lineno-cmd', + score: 1.4517338420208715e-5 + }, + { + caption: '\\nolinenumbers', + snippet: '\\nolinenumbers', + meta: 'lineno-cmd', + score: 0.0009805246614299932 + }, + { + caption: '\\path', + snippet: '\\path', + meta: 'lineno-cmd', + score: 0.028200474217322108 + }, + { + caption: '\\path[]', + snippet: '\\path[$1]', + meta: 'lineno-cmd', + score: 0.028200474217322108 + }, + { + caption: '\\path{}', + snippet: '\\path{$1}', + meta: 'lineno-cmd', + score: 0.028200474217322108 + }, + { + caption: '\\filedate{}', + snippet: '\\filedate{$1}', + meta: 'lineno-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\filedate', + snippet: '\\filedate', + meta: 'lineno-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\linenumbers', + snippet: '\\linenumbers', + meta: 'lineno-cmd', + score: 0.004687680659497865 + }, + { + caption: '\\modulolinenumbers[]', + snippet: '\\modulolinenumbers[$1]', + meta: 'lineno-cmd', + score: 0.0027194991933605197 + }, + { + caption: '\\fileversion{}', + snippet: '\\fileversion{$1}', + meta: 'lineno-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\fileversion', + snippet: '\\fileversion', + meta: 'lineno-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'lineno-cmd', + score: 0.008565354665444157 + } + ], + mathptmx: [ + { + caption: '\\rmdefault', + snippet: '\\rmdefault', + meta: 'mathptmx-cmd', + score: 0.0012870877747432935 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mathptmx-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mathptmx-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'mathptmx-cmd', + score: 0.05613164277964739 + } + ], + todonotes: [ + { + caption: '\\missingfigure[]{}', + snippet: '\\missingfigure[$1]{$2}', + meta: 'todonotes-cmd', + score: 0.001558719179721163 + }, + { + caption: '\\missingfigure', + snippet: '\\missingfigure', + meta: 'todonotes-cmd', + score: 0.001558719179721163 + }, + { + caption: '\\todototoc', + snippet: '\\todototoc', + meta: 'todonotes-cmd', + score: 0.000325977535138643 + }, + { + caption: '\\todo{}', + snippet: '\\todo{$1}', + meta: 'todonotes-cmd', + score: 0.04115074278362878 + }, + { + caption: '\\todo[]{}', + snippet: '\\todo[$1]{$2}', + meta: 'todonotes-cmd', + score: 0.04115074278362878 + }, + { + caption: '\\todo', + snippet: '\\todo', + meta: 'todonotes-cmd', + score: 0.04115074278362878 + }, + { + caption: '\\listoftodos', + snippet: '\\listoftodos', + meta: 'todonotes-cmd', + score: 0.0005325975940754609 + }, + { + caption: '\\listoftodos[]', + snippet: '\\listoftodos[$1]', + meta: 'todonotes-cmd', + score: 0.0005325975940754609 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'todonotes-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'todonotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'todonotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'todonotes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'todonotes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'todonotes-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'todonotes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'todonotes-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'todonotes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'todonotes-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'todonotes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'todonotes-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'todonotes-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'todonotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'todonotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'todonotes-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'todonotes-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'todonotes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'todonotes-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'todonotes-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'todonotes-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'todonotes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'todonotes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'todonotes-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'todonotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'todonotes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'todonotes-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'todonotes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'todonotes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'todonotes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'todonotes-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'todonotes-cmd', + score: 0.2864294797053033 + } + ], + ulem: [ + { + caption: '\\sout{}', + snippet: '\\sout{$1}', + meta: 'ulem-cmd', + score: 0.0010443313503631364 + }, + { + caption: '\\sout', + snippet: '\\sout', + meta: 'ulem-cmd', + score: 0.0010443313503631364 + }, + { + caption: '\\MakeRobust', + snippet: '\\MakeRobust', + meta: 'ulem-cmd', + score: 3.140504277052775e-5 + }, + { + caption: '\\hss', + snippet: '\\hss', + meta: 'ulem-cmd', + score: 0.0020627882815078768 + }, + { + caption: '\\uline{}', + snippet: '\\uline{$1}', + meta: 'ulem-cmd', + score: 0.005956273219192909 + }, + { + caption: '\\uline', + snippet: '\\uline', + meta: 'ulem-cmd', + score: 0.005956273219192909 + }, + { + caption: '\\markoverwith{}', + snippet: '\\markoverwith{$1}', + meta: 'ulem-cmd', + score: 0.0004888431085285657 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'ulem-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\hfill', + snippet: '\\hfill', + meta: 'ulem-cmd', + score: 0.2058248088519886 + }, + { + caption: '\\ULon', + snippet: '\\ULon', + meta: 'ulem-cmd', + score: 0.0004888431085285657 + }, + { + caption: '\\normalem', + snippet: '\\normalem', + meta: 'ulem-cmd', + score: 0.00015564484081028078 + }, + { + caption: '\\useunder{}{}{}', + snippet: '\\useunder{$1}{$2}{$3}', + meta: 'ulem-cmd', + score: 0.0013185833851097916 + }, + { + caption: '\\hfil', + snippet: '\\hfil', + meta: 'ulem-cmd', + score: 0.006880789969115855 + }, + { + caption: '\\sout{}', + snippet: '\\sout{$1}', + meta: 'ulem-cmd', + score: 0.0010443313503631364 + }, + { + caption: '\\sout', + snippet: '\\sout', + meta: 'ulem-cmd', + score: 0.0010443313503631364 + }, + { + caption: '\\MakeRobust', + snippet: '\\MakeRobust', + meta: 'ulem-cmd', + score: 3.140504277052775e-5 + }, + { + caption: '\\hss', + snippet: '\\hss', + meta: 'ulem-cmd', + score: 0.0020627882815078768 + }, + { + caption: '\\uline{}', + snippet: '\\uline{$1}', + meta: 'ulem-cmd', + score: 0.005956273219192909 + }, + { + caption: '\\uline', + snippet: '\\uline', + meta: 'ulem-cmd', + score: 0.005956273219192909 + }, + { + caption: '\\markoverwith{}', + snippet: '\\markoverwith{$1}', + meta: 'ulem-cmd', + score: 0.0004888431085285657 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'ulem-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\hfill', + snippet: '\\hfill', + meta: 'ulem-cmd', + score: 0.2058248088519886 + }, + { + caption: '\\ULon', + snippet: '\\ULon', + meta: 'ulem-cmd', + score: 0.0004888431085285657 + }, + { + caption: '\\normalem', + snippet: '\\normalem', + meta: 'ulem-cmd', + score: 0.00015564484081028078 + }, + { + caption: '\\useunder{}{}{}', + snippet: '\\useunder{$1}{$2}{$3}', + meta: 'ulem-cmd', + score: 0.0013185833851097916 + }, + { + caption: '\\hfil', + snippet: '\\hfil', + meta: 'ulem-cmd', + score: 0.006880789969115855 + } + ], + gensymb: [ + { + caption: '\\degree', + snippet: '\\degree', + meta: 'gensymb-cmd', + score: 0.044752043138360405 + }, + { + caption: '\\ohm', + snippet: '\\ohm', + meta: 'gensymb-cmd', + score: 0.0038146685721293138 + }, + { + caption: '\\micro', + snippet: '\\micro', + meta: 'gensymb-cmd', + score: 0.011051971930487929 + }, + { + caption: '\\celsius', + snippet: '\\celsius', + meta: 'gensymb-cmd', + score: 0.0010806983851157788 + } + ], + siunitx: [ + { + caption: '\\DeclareSIUnit{}{}', + snippet: '\\DeclareSIUnit{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.00017911905960739648 + }, + { + caption: '\\DeclareSIUnit', + snippet: '\\DeclareSIUnit', + meta: 'siunitx-cmd', + score: 0.00017911905960739648 + }, + { + caption: '\\si{}', + snippet: '\\si{$1}', + meta: 'siunitx-cmd', + score: 0.015042996547458706 + }, + { + caption: '\\num{}', + snippet: '\\num{$1}', + meta: 'siunitx-cmd', + score: 0.0005077454796577224 + }, + { + caption: '\\num[]{}', + snippet: '\\num[$1]{$2}', + meta: 'siunitx-cmd', + score: 0.0005077454796577224 + }, + { + caption: '\\ang{}', + snippet: '\\ang{$1}', + meta: 'siunitx-cmd', + score: 0.00026216419341458844 + }, + { + caption: '\\SIrange{}{}{}', + snippet: '\\SIrange{$1}{$2}{$3}', + meta: 'siunitx-cmd', + score: 0.0004920776847142836 + }, + { + caption: '\\SIrange[]{}{}{}', + snippet: '\\SIrange[$1]{$2}{$3}{$4}', + meta: 'siunitx-cmd', + score: 0.0004920776847142836 + }, + { + caption: '\\SIlist{}{}', + snippet: '\\SIlist{$1}{$2}', + meta: 'siunitx-cmd', + score: 2.5005836362206937e-5 + }, + { + caption: '\\SI{}{}', + snippet: '\\SI{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.04233098901537305 + }, + { + caption: '\\sisetup{}', + snippet: '\\sisetup{$1}', + meta: 'siunitx-cmd', + score: 0.0011875061630332172 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'siunitx-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'siunitx-cmd', + score: 0.0063276692758974925 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'siunitx-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'siunitx-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'siunitx-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'siunitx-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'siunitx-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'siunitx-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'siunitx-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'siunitx-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'siunitx-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'siunitx-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'siunitx-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'siunitx-cmd', + score: 0.2864294797053033 + } + ], + adjustbox: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'adjustbox-cmd', + score: 0.354445763583904 + }, + { + caption: '\\adjustbox{}{}', + snippet: '\\adjustbox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.002008185536556013 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'adjustbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'adjustbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'adjustbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'adjustbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'adjustbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'adjustbox-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'adjustbox-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'adjustbox-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'adjustbox-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'adjustbox-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'adjustbox-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'adjustbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'adjustbox-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'adjustbox-cmd', + score: 0.004649150613625593 + } + ], + moderncvcompatibility: [ + { + caption: '\\cvitem{}{}', + snippet: '\\cvitem{$1}{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.19605476980016281 + }, + { + caption: '\\cvlanguage{}{}{}', + snippet: '\\cvlanguage{$1}{$2}{$3}', + meta: 'moderncvcompatibility-cmd', + score: 0.00832363305853651 + }, + { + caption: '\\moderncvtheme[]{}', + snippet: '\\moderncvtheme[$1]{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.002355125248305291 + }, + { + caption: '\\moderncvtheme{}', + snippet: '\\moderncvtheme{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.002355125248305291 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'moderncvcompatibility-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\phone[]{}', + snippet: '\\phone[$1]{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.09602264063533228 + }, + { + caption: '\\moderncvstyle{}', + snippet: '\\moderncvstyle{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.09378844125415692 + }, + { + caption: '\\firstname{}', + snippet: '\\firstname{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.0070031590875754435 + }, + { + caption: '\\cvline{}{}', + snippet: '\\cvline{$1}{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.007378490468121007 + }, + { + caption: '\\mobile{}', + snippet: '\\mobile{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.022907406369946367 + }, + { + caption: '\\familyname{}', + snippet: '\\familyname{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.0070031590875754435 + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'moderncvcompatibility-cmd', + score: 3.0952612541683835 + } + ], + helvet: [ + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'helvet-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'helvet-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'helvet-cmd', + score: 0.00037306820619479756 + } + ], + placeins: [ + { + caption: '\\FloatBarrier', + snippet: '\\FloatBarrier', + meta: 'placeins-cmd', + score: 0.015841933780270347 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'placeins-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'placeins-cmd', + score: 0.021170869458413965 + } + ], + colortbl: [ + { + caption: '\\rowcolor{}', + snippet: '\\rowcolor{$1}', + meta: 'colortbl-cmd', + score: 0.05564476491638024 + }, + { + caption: '\\rowcolor[]{}', + snippet: '\\rowcolor[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.05564476491638024 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'colortbl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'colortbl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\arrayrulecolor{}', + snippet: '\\arrayrulecolor{$1}', + meta: 'colortbl-cmd', + score: 0.008538501902241319 + }, + { + caption: '\\arrayrulecolor[]{}', + snippet: '\\arrayrulecolor[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.008538501902241319 + }, + { + caption: '\\hline', + snippet: '\\hline', + meta: 'colortbl-cmd', + score: 1.3209538327406387 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\cellcolor[]{}', + snippet: '\\cellcolor[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.11068275858524645 + }, + { + caption: '\\cellcolor{}', + snippet: '\\cellcolor{$1}', + meta: 'colortbl-cmd', + score: 0.11068275858524645 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'colortbl-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'colortbl-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'colortbl-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'colortbl-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'colortbl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'colortbl-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'colortbl-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'colortbl-cmd', + score: 0.2864294797053033 + } + ], + appendix: [ + { + caption: '\\appendixpagename', + snippet: '\\appendixpagename', + meta: 'appendix-cmd', + score: 0.0005082989114039268 + }, + { + caption: '\\appendixpagename{}', + snippet: '\\appendixpagename{$1}', + meta: 'appendix-cmd', + score: 0.0005082989114039268 + }, + { + caption: '\\thechapter', + snippet: '\\thechapter', + meta: 'appendix-cmd', + score: 0.011821300392639589 + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'appendix-cmd', + score: 0.005008938879210868 + }, + { + caption: '\\thesubsection', + snippet: '\\thesubsection', + meta: 'appendix-cmd', + score: 0.004364729212023423 + }, + { + caption: '\\appendixname', + snippet: '\\appendixname', + meta: 'appendix-cmd', + score: 0.006491295958752496 + }, + { + caption: '\\appendixname{}', + snippet: '\\appendixname{$1}', + meta: 'appendix-cmd', + score: 0.006491295958752496 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'appendix-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\thesection', + snippet: '\\thesection', + meta: 'appendix-cmd', + score: 0.011068945893347528 + }, + { + caption: '\\thesection{}', + snippet: '\\thesection{$1}', + meta: 'appendix-cmd', + score: 0.011068945893347528 + }, + { + caption: '\\appendixpage', + snippet: '\\appendixpage', + meta: 'appendix-cmd', + score: 0.0003193786370376004 + }, + { + caption: '\\appendixpage{}', + snippet: '\\appendixpage{$1}', + meta: 'appendix-cmd', + score: 0.0003193786370376004 + }, + { + caption: '\\appendixtocname', + snippet: '\\appendixtocname', + meta: 'appendix-cmd', + score: 0.0005082989114039268 + }, + { + caption: '\\appendixtocname{}', + snippet: '\\appendixtocname{$1}', + meta: 'appendix-cmd', + score: 0.0005082989114039268 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'appendix-cmd', + score: 0.0174633138331273 + } + ], + supertabular: [ + { + caption: '\\tabletail{}', + snippet: '\\tabletail{$1}', + meta: 'supertabular-cmd', + score: 0.00284734590996941 + }, + { + caption: '\\tablehead{}', + snippet: '\\tablehead{$1}', + meta: 'supertabular-cmd', + score: 0.002940437317353234 + }, + { + caption: '\\tablelasttail{}', + snippet: '\\tablelasttail{$1}', + meta: 'supertabular-cmd', + score: 0.00284734590996941 + }, + { + caption: '\\tablefirsthead{}', + snippet: '\\tablefirsthead{$1}', + meta: 'supertabular-cmd', + score: 0.00284734590996941 + } + ], + makeidx: [ + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'makeidx-cmd', + score: 0.004417016910870522 + } + ], + framed: [ + { + caption: '\\fbox{}', + snippet: '\\fbox{$1}', + meta: 'framed-cmd', + score: 0.020865450075016792 + } + ], + layaureo: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'layaureo-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'layaureo-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'layaureo-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'layaureo-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'layaureo-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\savegeometry{}', + snippet: '\\savegeometry{$1}', + meta: 'layaureo-cmd', + score: 6.461638865465447e-5 + }, + { + caption: '\\loadgeometry{}', + snippet: '\\loadgeometry{$1}', + meta: 'layaureo-cmd', + score: 6.461638865465447e-5 + }, + { + caption: '\\newgeometry{}', + snippet: '\\newgeometry{$1}', + meta: 'layaureo-cmd', + score: 0.0025977479207639352 + }, + { + caption: '\\geometry{}', + snippet: '\\geometry{$1}', + meta: 'layaureo-cmd', + score: 0.046218420429973615 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'layaureo-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\restoregeometry', + snippet: '\\restoregeometry', + meta: 'layaureo-cmd', + score: 0.0007546303842143648 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'layaureo-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'layaureo-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'layaureo-cmd', + score: 0.002958865219480927 + } + ], + keyval: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'keyval-cmd', + score: 0.00037306820619479756 + } + ], + physics: [ + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'physics-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'physics-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\curl{}', + snippet: '\\curl{$1}', + meta: 'physics-cmd', + score: 0.001039136354388696 + }, + { + caption: '\\curl', + snippet: '\\curl', + meta: 'physics-cmd', + score: 0.001039136354388696 + }, + { + caption: '\\dd', + snippet: '\\dd', + meta: 'physics-cmd', + score: 0.0049652819784537965 + }, + { + caption: '\\expval{}', + snippet: '\\expval{$1}', + meta: 'physics-cmd', + score: 0.0006729185293892782 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'physics-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'physics-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\mqty', + snippet: '\\mqty', + meta: 'physics-cmd', + score: 0.0002048562866401335 + }, + { + caption: '\\order{}', + snippet: '\\order{$1}', + meta: 'physics-cmd', + score: 0.00019980403788140113 + }, + { + caption: '\\order', + snippet: '\\order', + meta: 'physics-cmd', + score: 0.00019980403788140113 + }, + { + caption: '\\abs{}', + snippet: '\\abs{$1}', + meta: 'physics-cmd', + score: 0.016268920166928613 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'physics-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'physics-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\dv{}{}', + snippet: '\\dv{$1}{$2}', + meta: 'physics-cmd', + score: 0.005139463745615663 + }, + { + caption: '\\dv[]{}{}', + snippet: '\\dv[$1]{$2}{$3}', + meta: 'physics-cmd', + score: 0.005139463745615663 + }, + { + caption: '\\eval{}', + snippet: '\\eval{$1}', + meta: 'physics-cmd', + score: 0.00021313621676565867 + }, + { + caption: '\\eval', + snippet: '\\eval', + meta: 'physics-cmd', + score: 0.00021313621676565867 + }, + { + caption: '\\eval[]{}', + snippet: '\\eval[$1]{$2}', + meta: 'physics-cmd', + score: 0.00021313621676565867 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'physics-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'physics-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ket{}', + snippet: '\\ket{$1}', + meta: 'physics-cmd', + score: 0.0326276280979336 + }, + { + caption: '\\mel{}{}{}', + snippet: '\\mel{$1}{$2}{$3}', + meta: 'physics-cmd', + score: 0.001123156900573353 + }, + { + caption: '\\ip', + snippet: '\\ip', + meta: 'physics-cmd', + score: 0.0008534664860896849 + }, + { + caption: '\\ip{}{}', + snippet: '\\ip{$1}{$2}', + meta: 'physics-cmd', + score: 0.0008534664860896849 + }, + { + caption: '\\ip[]{}', + snippet: '\\ip[$1]{$2}', + meta: 'physics-cmd', + score: 0.0008534664860896849 + }, + { + caption: '\\Im', + snippet: '\\Im', + meta: 'physics-cmd', + score: 0.0013451768070134808 + }, + { + caption: '\\Im{}', + snippet: '\\Im{$1}', + meta: 'physics-cmd', + score: 0.0013451768070134808 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'physics-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'physics-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\comm{}{}', + snippet: '\\comm{$1}{$2}', + meta: 'physics-cmd', + score: 0.0012026610554672049 + }, + { + caption: '\\qty', + snippet: '\\qty', + meta: 'physics-cmd', + score: 0.0017737618641299655 + }, + { + caption: '\\qty{}', + snippet: '\\qty{$1}', + meta: 'physics-cmd', + score: 0.0017737618641299655 + }, + { + caption: '\\Tr', + snippet: '\\Tr', + meta: 'physics-cmd', + score: 0.004615158124783136 + }, + { + caption: '\\Tr{}', + snippet: '\\Tr{$1}', + meta: 'physics-cmd', + score: 0.004615158124783136 + }, + { + caption: '\\bra{}', + snippet: '\\bra{$1}', + meta: 'physics-cmd', + score: 0.005609763332417241 + }, + { + caption: '\\poissonbracket{}{}', + snippet: '\\poissonbracket{$1}{$2}', + meta: 'physics-cmd', + score: 2.2761809626681494e-5 + }, + { + caption: '\\pmat{}', + snippet: '\\pmat{$1}', + meta: 'physics-cmd', + score: 0.00010356789132354732 + }, + { + caption: '\\norm{}', + snippet: '\\norm{$1}', + meta: 'physics-cmd', + score: 0.006576610603906938 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'physics-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'physics-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cross', + snippet: '\\cross', + meta: 'physics-cmd', + score: 0.0005412940211650938 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'physics-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\dmat{}', + snippet: '\\dmat{$1}', + meta: 'physics-cmd', + score: 2.2761809626681494e-5 + }, + { + caption: '\\Re', + snippet: '\\Re', + meta: 'physics-cmd', + score: 0.0031525922563281736 + }, + { + caption: '\\Re{}', + snippet: '\\Re{$1}', + meta: 'physics-cmd', + score: 0.0031525922563281736 + }, + { + caption: '\\qq{}', + snippet: '\\qq{$1}', + meta: 'physics-cmd', + score: 8.241282620919185e-5 + }, + { + caption: '\\qq', + snippet: '\\qq', + meta: 'physics-cmd', + score: 8.241282620919185e-5 + }, + { + caption: '\\vb{}', + snippet: '\\vb{$1}', + meta: 'physics-cmd', + score: 0.007377410801695042 + }, + { + caption: '\\pdv{}{}', + snippet: '\\pdv{$1}{$2}', + meta: 'physics-cmd', + score: 0.0014087913646471247 + }, + { + caption: '\\pdv{}{}{}', + snippet: '\\pdv{$1}{$2}{$3}', + meta: 'physics-cmd', + score: 0.0014087913646471247 + }, + { + caption: '\\braket{}{}', + snippet: '\\braket{$1}{$2}', + meta: 'physics-cmd', + score: 0.004421747491186916 + }, + { + caption: '\\braket{}', + snippet: '\\braket{$1}', + meta: 'physics-cmd', + score: 0.004421747491186916 + }, + { + caption: '\\div', + snippet: '\\div', + meta: 'physics-cmd', + score: 0.002403050103349905 + }, + { + caption: '\\div{}', + snippet: '\\div{$1}', + meta: 'physics-cmd', + score: 0.002403050103349905 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'physics-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'physics-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'physics-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'physics-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'physics-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'physics-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'physics-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'physics-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'physics-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'physics-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'physics-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'physics-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'physics-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'physics-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'physics-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'physics-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'physics-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'physics-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'physics-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'physics-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'physics-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'physics-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'physics-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'physics-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'physics-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'physics-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'physics-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'physics-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'physics-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'physics-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'physics-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'physics-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'physics-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'physics-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'physics-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'physics-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'physics-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'physics-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'physics-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'physics-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'physics-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'physics-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'physics-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'physics-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'physics-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'physics-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'physics-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'physics-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'physics-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'physics-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'physics-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'physics-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'physics-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'physics-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'physics-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'physics-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'physics-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'physics-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'physics-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'physics-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'physics-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'physics-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'physics-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'physics-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'physics-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'physics-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'physics-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'physics-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'physics-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'physics-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'physics-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'physics-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'physics-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'physics-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'physics-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'physics-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'physics-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'physics-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'physics-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'physics-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'physics-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'physics-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'physics-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'physics-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'physics-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'physics-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'physics-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'physics-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'physics-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'physics-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'physics-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'physics-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'physics-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'physics-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'physics-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'physics-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'physics-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'physics-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'physics-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'physics-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'physics-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'physics-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'physics-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'physics-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'physics-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'physics-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'physics-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'physics-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'physics-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'physics-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'physics-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'physics-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'physics-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'physics-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'physics-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'physics-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'physics-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'physics-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'physics-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'physics-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'physics-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'physics-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'physics-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'physics-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'physics-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'physics-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'physics-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'physics-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'physics-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'physics-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'physics-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'physics-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'physics-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'physics-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'physics-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'physics-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'physics-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'physics-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'physics-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'physics-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'physics-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'physics-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'physics-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'physics-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'physics-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'physics-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'physics-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'physics-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'physics-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'physics-cmd', + score: 0.0063276692758974925 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'physics-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'physics-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'physics-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'physics-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'physics-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'physics-cmd', + score: 0.2864294797053033 + } + ], + authblk: [ + { + caption: '\\Authfont{}', + snippet: '\\Authfont{$1}', + meta: 'authblk-cmd', + score: 0.00019538157043798684 + }, + { + caption: '\\thanks{}', + snippet: '\\thanks{$1}', + meta: 'authblk-cmd', + score: 0.08382259880654083 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'authblk-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\rlap{}', + snippet: '\\rlap{$1}', + meta: 'authblk-cmd', + score: 0.01269300721396509 + }, + { + caption: '\\Authands{}', + snippet: '\\Authands{$1}', + meta: 'authblk-cmd', + score: 0.00043932814970131613 + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'authblk-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'authblk-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'authblk-cmd', + score: 0.05216393882408519 + }, + { + caption: '\\Affilfont{}', + snippet: '\\Affilfont{$1}', + meta: 'authblk-cmd', + score: 0.0004505484831792931 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'authblk-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\affil[]{}', + snippet: '\\affil[$1]{$2}', + meta: 'authblk-cmd', + score: 0.014174618039587864 + }, + { + caption: '\\affil{}', + snippet: '\\affil{$1}', + meta: 'authblk-cmd', + score: 0.014174618039587864 + } + ], + tabu: [ + { + caption: '\\extrarowheight', + snippet: '\\extrarowheight', + meta: 'tabu-cmd', + score: 0.003735645243417412 + }, + { + caption: '\\extrarowheight{}', + snippet: '\\extrarowheight{$1}', + meta: 'tabu-cmd', + score: 0.003735645243417412 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabu-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tabu-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\hfill', + snippet: '\\hfill', + meta: 'tabu-cmd', + score: 0.2058248088519886 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'tabu-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'tabu-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\tabulinesep', + snippet: '\\tabulinesep', + meta: 'tabu-cmd', + score: 0.0008256968285249214 + }, + { + caption: '\\hskip', + snippet: '\\hskip', + meta: 'tabu-cmd', + score: 0.04339822811565144 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'tabu-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabu-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'tabu-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabu-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'tabu-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tabu-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'tabu-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'tabu-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'tabu-cmd', + score: 0.413853376001159 + } + ], + CJKutf8: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'CJKutf8-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'CJKutf8-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'CJKutf8-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'CJKutf8-cmd', + score: 0.04598628699063736 + }, + { + caption: '\\inputencoding{}', + snippet: '\\inputencoding{$1}', + meta: 'CJKutf8-cmd', + score: 0.0002447047447770061 + } + ], + sectsty: [ + { + caption: '\\chapterfont{}', + snippet: '\\chapterfont{$1}', + meta: 'sectsty-cmd', + score: 0.0001572081344977262 + }, + { + caption: '\\raggedright', + snippet: '\\raggedright', + meta: 'sectsty-cmd', + score: 0.05314494127699766 + }, + { + caption: '\\sectionfont{}', + snippet: '\\sectionfont{$1}', + meta: 'sectsty-cmd', + score: 0.003867941482301249 + }, + { + caption: '\\paragraph{}', + snippet: '\\paragraph{$1}', + meta: 'sectsty-cmd', + score: 0.152074250347974 + }, + { + caption: '\\allsectionsfont{}', + snippet: '\\allsectionsfont{$1}', + meta: 'sectsty-cmd', + score: 0.0011367198619746117 + }, + { + caption: '\\subsection{}', + snippet: '\\subsection{$1}', + meta: 'sectsty-cmd', + score: 1.3890912739512353 + }, + { + caption: '\\subsectionfont{}', + snippet: '\\subsectionfont{$1}', + meta: 'sectsty-cmd', + score: 0.002811633808315226 + }, + { + caption: '\\interlinepenalty', + snippet: '\\interlinepenalty', + meta: 'sectsty-cmd', + score: 0.00032069955588347133 + }, + { + caption: '\\subsubsectionfont{}', + snippet: '\\subsubsectionfont{$1}', + meta: 'sectsty-cmd', + score: 0.0011363939259266408 + }, + { + caption: '\\underline{}', + snippet: '\\underline{$1}', + meta: 'sectsty-cmd', + score: 0.14748550887002482 + }, + { + caption: '\\subsubsection{}', + snippet: '\\subsubsection{$1}', + meta: 'sectsty-cmd', + score: 0.3727781330132016 + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'sectsty-cmd', + score: 3.0952612541683835 + } + ], + lscape: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'lscape-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lscape-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lscape-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'lscape-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'lscape-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'lscape-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'lscape-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'lscape-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'lscape-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'lscape-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'lscape-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'lscape-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'lscape-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'lscape-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'lscape-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'lscape-cmd', + score: 0.004649150613625593 + } + ], + hyphenat: [ + { + caption: '\\hyp{}', + snippet: '\\hyp{$1}', + meta: 'hyphenat-cmd', + score: 0.0013359874951570454 + } + ], + tocloft: [ + { + caption: '\\cftsecleader', + snippet: '\\cftsecleader', + meta: 'tocloft-cmd', + score: 0.0011340882025681251 + }, + { + caption: '\\cftloftitlefont', + snippet: '\\cftloftitlefont', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\cftchappresnum{}', + snippet: '\\cftchappresnum{$1}', + meta: 'tocloft-cmd', + score: 2.8671864736205568e-5 + }, + { + caption: '\\cftchappresnum', + snippet: '\\cftchappresnum', + meta: 'tocloft-cmd', + score: 2.8671864736205568e-5 + }, + { + caption: '\\listoftables', + snippet: '\\listoftables', + meta: 'tocloft-cmd', + score: 0.02104656820469027 + }, + { + caption: '\\cftsecfont{}', + snippet: '\\cftsecfont{$1}', + meta: 'tocloft-cmd', + score: 5.630015640183448e-5 + }, + { + caption: '\\cftchapfont{}', + snippet: '\\cftchapfont{$1}', + meta: 'tocloft-cmd', + score: 6.253521408609416e-5 + }, + { + caption: '\\cftchapfont', + snippet: '\\cftchapfont', + meta: 'tocloft-cmd', + score: 6.253521408609416e-5 + }, + { + caption: '\\cftsubsecleader', + snippet: '\\cftsubsecleader', + meta: 'tocloft-cmd', + score: 1.0644172549700836e-5 + }, + { + caption: '\\cftchapleader', + snippet: '\\cftchapleader', + meta: 'tocloft-cmd', + score: 1.0644172549700836e-5 + }, + { + caption: '\\tocloftpagestyle{}', + snippet: '\\tocloftpagestyle{$1}', + meta: 'tocloft-cmd', + score: 8.392451158032374e-5 + }, + { + caption: '\\cfttoctitlefont', + snippet: '\\cfttoctitlefont', + meta: 'tocloft-cmd', + score: 6.877027177035383e-5 + }, + { + caption: '\\cftdot', + snippet: '\\cftdot', + meta: 'tocloft-cmd', + score: 1.6201749367686227e-5 + }, + { + caption: '\\cftsecdotsep', + snippet: '\\cftsecdotsep', + meta: 'tocloft-cmd', + score: 0.0029383990986223767 + }, + { + caption: '\\cftafterloftitle', + snippet: '\\cftafterloftitle', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\listoffigures', + snippet: '\\listoffigures', + meta: 'tocloft-cmd', + score: 0.03447318897846567 + }, + { + caption: '\\cftdotfill{}', + snippet: '\\cftdotfill{$1}', + meta: 'tocloft-cmd', + score: 0.006027562229085753 + }, + { + caption: '\\tableofcontents', + snippet: '\\tableofcontents', + meta: 'tocloft-cmd', + score: 0.13360595130994957 + }, + { + caption: '\\cftdotsep', + snippet: '\\cftdotsep', + meta: 'tocloft-cmd', + score: 0.003089163130463376 + }, + { + caption: '\\numberline{}', + snippet: '\\numberline{$1}', + meta: 'tocloft-cmd', + score: 0.007461440567272885 + }, + { + caption: '\\cftlottitlefont', + snippet: '\\cftlottitlefont', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\cftchappagefont{}', + snippet: '\\cftchappagefont{$1}', + meta: 'tocloft-cmd', + score: 5.630015640183448e-5 + }, + { + caption: '\\cftsetindents{}{}{}', + snippet: '\\cftsetindents{$1}{$2}{$3}', + meta: 'tocloft-cmd', + score: 0.00043647269161217853 + }, + { + caption: '\\cftsecpagefont{}', + snippet: '\\cftsecpagefont{$1}', + meta: 'tocloft-cmd', + score: 5.630015640183448e-5 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'tocloft-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\cftaftertoctitle', + snippet: '\\cftaftertoctitle', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\cftafterlottitle', + snippet: '\\cftafterlottitle', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\newlistof{}{}{}', + snippet: '\\newlistof{$1}{$2}{$3}', + meta: 'tocloft-cmd', + score: 0.0005381264966408724 + } + ], + glossaries: [ + { + caption: '\\glslongpluralkey', + snippet: '\\glslongpluralkey', + meta: 'glossaries-cmd', + score: 1.4538687447297259e-5 + }, + { + caption: '\\Glspl{}', + snippet: '\\Glspl{$1}', + meta: 'glossaries-cmd', + score: 0.0025291265119320736 + }, + { + caption: '\\glossarysection', + snippet: '\\glossarysection', + meta: 'glossaries-cmd', + score: 9.579755294730752e-5 + }, + { + caption: '\\printglossaries', + snippet: '\\printglossaries', + meta: 'glossaries-cmd', + score: 0.0010106582768889887 + }, + { + caption: '\\Gls{}', + snippet: '\\Gls{$1}', + meta: 'glossaries-cmd', + score: 0.003696678698317109 + }, + { + caption: '\\setglossarystyle{}', + snippet: '\\setglossarystyle{$1}', + meta: 'glossaries-cmd', + score: 0.0003758893277679221 + }, + { + caption: '\\printglossary', + snippet: '\\printglossary', + meta: 'glossaries-cmd', + score: 0.009139682306158714 + }, + { + caption: '\\printglossary[]', + snippet: '\\printglossary[$1]', + meta: 'glossaries-cmd', + score: 0.009139682306158714 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\setglossarysection{}', + snippet: '\\setglossarysection{$1}', + meta: 'glossaries-cmd', + score: 3.6081414102781514e-5 + }, + { + caption: '\\glsresetall', + snippet: '\\glsresetall', + meta: 'glossaries-cmd', + score: 0.0006123462672467326 + }, + { + caption: '\\the', + snippet: '\\the', + meta: 'glossaries-cmd', + score: 0.007238960303946444 + }, + { + caption: '\\acrshort{}', + snippet: '\\acrshort{$1}', + meta: 'glossaries-cmd', + score: 0.009936841864059727 + }, + { + caption: '\\printnoidxglossary[]', + snippet: '\\printnoidxglossary[$1]', + meta: 'glossaries-cmd', + score: 0.00021912375285685037 + }, + { + caption: '\\newglossary{}{}', + snippet: '\\newglossary{$1}{$2}', + meta: 'glossaries-cmd', + score: 1.4547244650032571e-5 + }, + { + caption: '\\gls{}', + snippet: '\\gls{$1}', + meta: 'glossaries-cmd', + score: 0.06939353309055077 + }, + { + caption: '\\printnoidxglossaries', + snippet: '\\printnoidxglossaries', + meta: 'glossaries-cmd', + score: 5.6789564226023136e-5 + }, + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'glossaries-cmd', + score: 0.004417016910870522 + }, + { + caption: '\\defglsentryfmt[]{}', + snippet: '\\defglsentryfmt[$1]{$2}', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\glspostdescription', + snippet: '\\glspostdescription', + meta: 'glossaries-cmd', + score: 0.0006337376579591112 + }, + { + caption: '\\number', + snippet: '\\number', + meta: 'glossaries-cmd', + score: 0.000968714260809983 + }, + { + caption: '\\glsaddall', + snippet: '\\glsaddall', + meta: 'glossaries-cmd', + score: 0.0008363820557740373 + }, + { + caption: '\\glsaddall[]', + snippet: '\\glsaddall[$1]', + meta: 'glossaries-cmd', + score: 0.0008363820557740373 + }, + { + caption: '\\makeglossaries', + snippet: '\\makeglossaries', + meta: 'glossaries-cmd', + score: 0.0056737600836936995 + }, + { + caption: '\\glossaryname', + snippet: '\\glossaryname', + meta: 'glossaries-cmd', + score: 0.0006174536302752427 + }, + { + caption: '\\newglossaryentry{}{}', + snippet: '\\newglossaryentry{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.018524394136900962 + }, + { + caption: '\\glslabel', + snippet: '\\glslabel', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\glsadd{}', + snippet: '\\glsadd{$1}', + meta: 'glossaries-cmd', + score: 3.0150373480213892e-5 + }, + { + caption: '\\makenoidxglossaries', + snippet: '\\makenoidxglossaries', + meta: 'glossaries-cmd', + score: 0.0001382210125680805 + }, + { + caption: '\\glsgenentryfmt', + snippet: '\\glsgenentryfmt', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\acronymtype', + snippet: '\\acronymtype', + meta: 'glossaries-cmd', + score: 0.002000834271117562 + }, + { + caption: '\\acrfull{}', + snippet: '\\acrfull{$1}', + meta: 'glossaries-cmd', + score: 0.0032622587277765067 + }, + { + caption: '\\newacronym{}{}{}', + snippet: '\\newacronym{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.03193935544723102 + }, + { + caption: '\\glspl{}', + snippet: '\\glspl{$1}', + meta: 'glossaries-cmd', + score: 0.0034025897522047717 + }, + { + caption: '\\ifglsused{}{}{}', + snippet: '\\ifglsused{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\acrlong{}', + snippet: '\\acrlong{$1}', + meta: 'glossaries-cmd', + score: 0.002517821598213752 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'glossaries-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'glossaries-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'glossaries-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'glossaries-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'glossaries-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'glossaries-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'glossaries-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'glossaries-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'glossaries-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'glossaries-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'glossaries-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'glossaries-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'glossaries-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'glossaries-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'glossaries-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'glossaries-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'glossaries-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'glossaries-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'glossaries-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'glossaries-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'glossaries-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'glossaries-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'glossaries-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'glossaries-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'glossaries-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'glossaries-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'glossaries-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'glossaries-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'glossaries-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'glossaries-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'glossaries-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'glossaries-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'glossaries-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'glossaries-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'glossaries-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'glossaries-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'glossaries-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'glossaries-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'glossaries-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'glossaries-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'glossaries-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'glossaries-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'glossaries-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'glossaries-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'glossaries-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'glossaries-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'glossaries-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'glossaries-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'glossaries-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'glossaries-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'glossaries-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'glossaries-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'glossaries-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'glossaries-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'glossaries-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'glossaries-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'glossaries-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'glossaries-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'glossaries-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'glossaries-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'glossaries-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'glossaries-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'glossaries-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'glossaries-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'glossaries-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'glossaries-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'glossaries-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'glossaries-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'glossaries-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'glossaries-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'glossaries-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'glossaries-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'glossaries-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'glossaries-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'glossaries-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'glossaries-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'glossaries-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'glossaries-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'glossaries-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'glossaries-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'glossaries-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'glossaries-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'glossaries-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'glossaries-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'glossaries-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'glossaries-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'glossaries-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'glossaries-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'glossaries-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'glossaries-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'glossaries-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'glossaries-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'glossaries-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'glossaries-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'glossaries-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'glossaries-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'glossaries-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'glossaries-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'glossaries-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'glossaries-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'glossaries-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'glossaries-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'glossaries-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'glossaries-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'glossaries-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'glossaries-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'glossaries-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'glossaries-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'glossaries-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'glossaries-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'glossaries-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'glossaries-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'glossaries-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'glossaries-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'glossaries-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'glossaries-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'glossaries-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'glossaries-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'glossaries-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'glossaries-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'glossaries-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'glossaries-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'glossaries-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'glossaries-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'glossaries-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'glossaries-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'glossaries-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'glossaries-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'glossaries-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'glossaries-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'glossaries-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'glossaries-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'glossaries-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'glossaries-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'glossaries-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'glossaries-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'glossaries-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'glossaries-cmd', + score: 2.341195220791228 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'glossaries-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'glossaries-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'glossaries-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'glossaries-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'glossaries-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'glossaries-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'glossaries-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'glossaries-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'glossaries-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'glossaries-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'glossaries-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'glossaries-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'glossaries-cmd', + score: 0.0063276692758974925 + } + ], + cleveref: [ + { + caption: '\\crefdefaultlabelformat{}', + snippet: '\\crefdefaultlabelformat{$1}', + meta: 'cleveref-cmd', + score: 8.401009062000455e-6 + }, + { + caption: '\\crefname{}{}{}', + snippet: '\\crefname{$1}{$2}{$3}', + meta: 'cleveref-cmd', + score: 0.0016963440482621792 + }, + { + caption: '\\crefrangeformat{}{}', + snippet: '\\crefrangeformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'cleveref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'cleveref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\crefmultiformat{}{}', + snippet: '\\crefmultiformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\crefformat{}{}', + snippet: '\\crefformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.0006776840671975755 + }, + { + caption: '\\Cref{}', + snippet: '\\Cref{$1}', + meta: 'cleveref-cmd', + score: 0.0016649686371949341 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'cleveref-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\cref{}', + snippet: '\\cref{$1}', + meta: 'cleveref-cmd', + score: 0.0159491058092361 + }, + { + caption: '\\crefrangeconjunction', + snippet: '\\crefrangeconjunction', + meta: 'cleveref-cmd', + score: 3.2405622997778076e-6 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cleveref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\creflabelformat{}{}', + snippet: '\\creflabelformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.000997031755478214 + }, + { + caption: '\\Crefname{}{}{}', + snippet: '\\Crefname{$1}{$2}{$3}', + meta: 'cleveref-cmd', + score: 0.000239288793927364 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'cleveref-cmd', + score: 1.897791904799601 + }, + { + caption: '\\labelcref{}', + snippet: '\\labelcref{$1}', + meta: 'cleveref-cmd', + score: 6.720807249600364e-5 + }, + { + caption: '\\creflastconjunction', + snippet: '\\creflastconjunction', + meta: 'cleveref-cmd', + score: 3.2405622997778076e-6 + } + ], + 'eso-pic': [ + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'eso-pic-cmd', + score: 0.000325977535138643 + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'eso-pic-cmd', + score: 0.0008957666085644653 + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'eso-pic-cmd', + score: 0.0003608141410278152 + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'eso-pic-cmd', + score: 0.0007216282820556304 + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'eso-pic-cmd', + score: 0.0017658629469099734 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'eso-pic-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'eso-pic-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'eso-pic-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'eso-pic-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'eso-pic-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'eso-pic-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eso-pic-cmd', + score: 0.008565354665444157 + } + ], + mhchem: [ + { + caption: '\\ce{}', + snippet: '\\ce{$1}', + meta: 'mhchem-cmd', + score: 0.04246600383063094 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mhchem-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mhchem-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'mhchem-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'mhchem-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'mhchem-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'mhchem-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mhchem-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'mhchem-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mhchem-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'mhchem-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'mhchem-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'mhchem-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'mhchem-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'mhchem-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mhchem-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'mhchem-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'mhchem-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'mhchem-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'mhchem-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'mhchem-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'mhchem-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'mhchem-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'mhchem-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'mhchem-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'mhchem-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'mhchem-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'mhchem-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'mhchem-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'mhchem-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'mhchem-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'mhchem-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'mhchem-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'mhchem-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'mhchem-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'mhchem-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'mhchem-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'mhchem-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'mhchem-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'mhchem-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'mhchem-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'mhchem-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'mhchem-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'mhchem-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'mhchem-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'mhchem-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'mhchem-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'mhchem-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'mhchem-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'mhchem-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'mhchem-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'mhchem-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'mhchem-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'mhchem-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'mhchem-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'mhchem-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mhchem-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'mhchem-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'mhchem-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'mhchem-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'mhchem-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'mhchem-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'mhchem-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'mhchem-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'mhchem-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'mhchem-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'mhchem-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'mhchem-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'mhchem-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'mhchem-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'mhchem-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'mhchem-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'mhchem-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'mhchem-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'mhchem-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'mhchem-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'mhchem-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'mhchem-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'mhchem-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'mhchem-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'mhchem-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'mhchem-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'mhchem-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'mhchem-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'mhchem-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'mhchem-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mhchem-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'mhchem-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'mhchem-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'mhchem-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'mhchem-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'mhchem-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'mhchem-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'mhchem-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'mhchem-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'mhchem-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'mhchem-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'mhchem-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'mhchem-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'mhchem-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'mhchem-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'mhchem-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'mhchem-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'mhchem-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'mhchem-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'mhchem-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'mhchem-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'mhchem-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'mhchem-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'mhchem-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'mhchem-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'mhchem-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'mhchem-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'mhchem-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'mhchem-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'mhchem-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'mhchem-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'mhchem-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'mhchem-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'mhchem-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'mhchem-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'mhchem-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'mhchem-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'mhchem-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'mhchem-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'mhchem-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'mhchem-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'mhchem-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'mhchem-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'mhchem-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'mhchem-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'mhchem-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'mhchem-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'mhchem-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'mhchem-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'mhchem-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'mhchem-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'mhchem-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'mhchem-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'mhchem-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'mhchem-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'mhchem-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'mhchem-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'mhchem-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mhchem-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'mhchem-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mhchem-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mhchem-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'mhchem-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'mhchem-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'mhchem-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'mhchem-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mhchem-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mhchem-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mhchem-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'mhchem-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mhchem-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mhchem-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'mhchem-cmd', + score: 0.0063276692758974925 + } + ], + amscd: [ + { + caption: '\\tag{}', + snippet: '\\tag{$1}', + meta: 'amscd-cmd', + score: 0.00784357461002059 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amscd-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amscd-cmd', + score: 0.0063276692758974925 + } + ], + 'unicode-math': [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'unicode-math-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'unicode-math-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'unicode-math-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'unicode-math-cmd', + score: 0.2864294797053033 + } + ], + ifxetex: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ifxetex-cmd', + score: 0.00021116765384691477 + } + ], + newtxmath: [ + { + caption: '\\int', + snippet: '\\int', + meta: 'newtxmath-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\sqrt{}', + snippet: '\\sqrt{$1}', + meta: 'newtxmath-cmd', + score: 0.20240160977404634 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'newtxmath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\hbar', + snippet: '\\hbar', + meta: 'newtxmath-cmd', + score: 0.024733493787737763 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'newtxmath-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\surd', + snippet: '\\surd', + meta: 'newtxmath-cmd', + score: 0.002159694087964359 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'newtxmath-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'newtxmath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'newtxmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'newtxmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\vdots', + snippet: '\\vdots', + meta: 'newtxmath-cmd', + score: 0.03669355896719803 + }, + { + caption: '\\ddots', + snippet: '\\ddots', + meta: 'newtxmath-cmd', + score: 0.010831382784078964 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxmath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'newtxmath-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'newtxmath-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'newtxmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'newtxmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'newtxmath-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'newtxmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'newtxmath-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'newtxmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'newtxmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'newtxmath-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'newtxmath-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'newtxmath-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'newtxmath-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'newtxmath-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'newtxmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'newtxmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'newtxmath-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'newtxmath-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'newtxmath-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'newtxmath-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'newtxmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'newtxmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'newtxmath-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'newtxmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'newtxmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'newtxmath-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'newtxmath-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'newtxmath-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'newtxmath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'newtxmath-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'newtxmath-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'newtxmath-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'newtxmath-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'newtxmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'newtxmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'newtxmath-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'newtxmath-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'newtxmath-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'newtxmath-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'newtxmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'newtxmath-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'newtxmath-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'newtxmath-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'newtxmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'newtxmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'newtxmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'newtxmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'newtxmath-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'newtxmath-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'newtxmath-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'newtxmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'newtxmath-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'newtxmath-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'newtxmath-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'newtxmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'newtxmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'newtxmath-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'newtxmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'newtxmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'newtxmath-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'newtxmath-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'newtxmath-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'newtxmath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'newtxmath-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'newtxmath-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'newtxmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'newtxmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'newtxmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'newtxmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'newtxmath-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'newtxmath-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'newtxmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'newtxmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'newtxmath-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'newtxmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'newtxmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'newtxmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'newtxmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'newtxmath-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'newtxmath-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'newtxmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'newtxmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'newtxmath-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'newtxmath-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'newtxmath-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'newtxmath-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'newtxmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'newtxmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'newtxmath-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'newtxmath-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'newtxmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'newtxmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'newtxmath-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'newtxmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'newtxmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'newtxmath-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'newtxmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'newtxmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'newtxmath-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'newtxmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'newtxmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'newtxmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'newtxmath-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'newtxmath-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'newtxmath-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'newtxmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'newtxmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'newtxmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'newtxmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'newtxmath-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'newtxmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'newtxmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'newtxmath-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'newtxmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'newtxmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'newtxmath-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'newtxmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'newtxmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'newtxmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'newtxmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'newtxmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'newtxmath-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'newtxmath-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'newtxmath-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'newtxmath-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'newtxmath-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'newtxmath-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'newtxmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'newtxmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'newtxmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'newtxmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'newtxmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'newtxmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'newtxmath-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'newtxmath-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'newtxmath-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'newtxmath-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'newtxmath-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'newtxmath-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxmath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxmath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'newtxmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'newtxmath-cmd', + score: 0.0063276692758974925 + } + ], + pdflscape: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdflscape-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'pdflscape-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdflscape-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdflscape-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pdflscape-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pdflscape-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pdflscape-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pdflscape-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pdflscape-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pdflscape-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pdflscape-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdflscape-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pdflscape-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pdflscape-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pdflscape-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pdflscape-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pdflscape-cmd', + score: 0.004649150613625593 + } + ], + apacite: [ + { + caption: '\\citep{}', + snippet: '\\citep{$1}', + meta: 'apacite-cmd', + score: 0.2941882834697057 + }, + { + caption: '\\citet{}', + snippet: '\\citet{$1}', + meta: 'apacite-cmd', + score: 0.09046048561361801 + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'apacite-cmd', + score: 0.13586474005868793 + }, + { + caption: '\\BPGS', + snippet: '\\BPGS', + meta: 'apacite-cmd', + score: 0.00023651453263545777 + }, + { + caption: '\\shortcite{}', + snippet: '\\shortcite{$1}', + meta: 'apacite-cmd', + score: 0.010082057767216608 + }, + { + caption: '\\shortciteA{}', + snippet: '\\shortciteA{$1}', + meta: 'apacite-cmd', + score: 0.0011019769466422762 + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'apacite-cmd', + score: 0.04990693820960752 + }, + { + caption: '\\refname', + snippet: '\\refname', + meta: 'apacite-cmd', + score: 0.006490238196722249 + }, + { + caption: '\\refname{}', + snippet: '\\refname{$1}', + meta: 'apacite-cmd', + score: 0.006490238196722249 + }, + { + caption: '\\citeA{}', + snippet: '\\citeA{$1}', + meta: 'apacite-cmd', + score: 0.008470555729707068 + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'apacite-cmd', + score: 0.01091041305836494 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'apacite-cmd', + score: 2.341195220791228 + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'apacite-cmd', + score: 0.2659628337907604 + }, + { + caption: '\\BPG', + snippet: '\\BPG', + meta: 'apacite-cmd', + score: 0.00023651453263545777 + }, + { + caption: '\\citeNP{}', + snippet: '\\citeNP{$1}', + meta: 'apacite-cmd', + score: 0.0003168688289795556 + }, + { + caption: '\\citeauthor{}', + snippet: '\\citeauthor{$1}', + meta: 'apacite-cmd', + score: 0.01359248786373484 + } + ], + mathpazo: [ + { + caption: '\\big', + snippet: '\\big', + meta: 'mathpazo-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\mathbb{}', + snippet: '\\mathbb{$1}', + meta: 'mathpazo-cmd', + score: 0.33740449739178857 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mathpazo-cmd', + score: 0.050370758781422345 + } + ], + footmisc: [ + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'footmisc-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'footmisc-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'footmisc-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\multfootsep', + snippet: '\\multfootsep', + meta: 'footmisc-cmd', + score: 0.00010171098214158578 + }, + { + caption: '\\footnotelayout', + snippet: '\\footnotelayout', + meta: 'footmisc-cmd', + score: 0.0004535003423927585 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'footmisc-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'footmisc-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'footmisc-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\thefootnote', + snippet: '\\thefootnote', + meta: 'footmisc-cmd', + score: 0.007676927812687567 + }, + { + caption: '\\thefootnote{}', + snippet: '\\thefootnote{$1}', + meta: 'footmisc-cmd', + score: 0.007676927812687567 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'footmisc-cmd', + score: 0.1789117552185788 + } + ], + fixltx2e: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'fixltx2e-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'fixltx2e-cmd', + score: 0.354445763583904 + }, + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'fixltx2e-cmd', + score: 0.058405875394131175 + }, + { + caption: '\\em', + snippet: '\\em', + meta: 'fixltx2e-cmd', + score: 0.10357353994640862 + } + ], + sidecap: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'sidecap-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'sidecap-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\sidecaptionvpos{}{}', + snippet: '\\sidecaptionvpos{$1}{$2}', + meta: 'sidecap-cmd', + score: 0.0006587927449241846 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'sidecap-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'sidecap-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'sidecap-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'sidecap-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'sidecap-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'sidecap-cmd', + score: 0.0018957469739775527 + } + ], + nomencl: [ + { + caption: '\\nomenclature[]{}{}', + snippet: '\\nomenclature[$1]{$2}{$3}', + meta: 'nomencl-cmd', + score: 0.016053526743355948 + }, + { + caption: '\\nomenclature{}{}', + snippet: '\\nomenclature{$1}{$2}', + meta: 'nomencl-cmd', + score: 0.016053526743355948 + }, + { + caption: '\\nomlabel', + snippet: '\\nomlabel', + meta: 'nomencl-cmd', + score: 6.353668036093916e-5 + }, + { + caption: '\\printnomenclature', + snippet: '\\printnomenclature', + meta: 'nomencl-cmd', + score: 0.0014526113324237952 + }, + { + caption: '\\printnomenclature[]', + snippet: '\\printnomenclature[$1]', + meta: 'nomencl-cmd', + score: 0.0014526113324237952 + }, + { + caption: '\\makenomenclature', + snippet: '\\makenomenclature', + meta: 'nomencl-cmd', + score: 0.002310610204652063 + }, + { + caption: '\\nomgroup', + snippet: '\\nomgroup', + meta: 'nomencl-cmd', + score: 0.0005549290951493257 + }, + { + caption: '\\nomgroup[]{}', + snippet: '\\nomgroup[$1]{$2}', + meta: 'nomencl-cmd', + score: 0.0005549290951493257 + }, + { + caption: '\\nomname', + snippet: '\\nomname', + meta: 'nomencl-cmd', + score: 0.0015092617929470952 + }, + { + caption: '\\nompreamble', + snippet: '\\nompreamble', + meta: 'nomencl-cmd', + score: 2.4350510995473236e-5 + }, + { + caption: '\\nomentryend', + snippet: '\\nomentryend', + meta: 'nomencl-cmd', + score: 0.000137692304514793 + } + ], + afterpage: [ + { + caption: '\\afterpage{}', + snippet: '\\afterpage{$1}', + meta: 'afterpage-cmd', + score: 0.0018578070791608345 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'afterpage-cmd', + score: 0.1789117552185788 + } + ], + titling: [ + { + caption: '\\thanks{}', + snippet: '\\thanks{$1}', + meta: 'titling-cmd', + score: 0.08382259880654083 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'titling-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\posttitle{}', + snippet: '\\posttitle{$1}', + meta: 'titling-cmd', + score: 0.002507149245154055 + }, + { + caption: '\\postdate{}', + snippet: '\\postdate{$1}', + meta: 'titling-cmd', + score: 0.002139478682489868 + }, + { + caption: '\\predate{}', + snippet: '\\predate{$1}', + meta: 'titling-cmd', + score: 0.002139478682489868 + }, + { + caption: '\\preauthor{}', + snippet: '\\preauthor{$1}', + meta: 'titling-cmd', + score: 0.0023736543205198435 + }, + { + caption: '\\postauthor{}', + snippet: '\\postauthor{$1}', + meta: 'titling-cmd', + score: 0.0023736543205198435 + }, + { + caption: '\\pretitle{}', + snippet: '\\pretitle{$1}', + meta: 'titling-cmd', + score: 0.002507149245154055 + } + ], + wasysym: [ + { + caption: '\\checked', + snippet: '\\checked', + meta: 'wasysym-cmd', + score: 0.0027792832228568255 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'wasysym-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\diameter', + snippet: '\\diameter', + meta: 'wasysym-cmd', + score: 0.0001645367385856751 + }, + { + caption: '\\CIRCLE', + snippet: '\\CIRCLE', + meta: 'wasysym-cmd', + score: 0.000250667024953401 + } + ], + eurosym: [ + { + caption: '\\EUR{}', + snippet: '\\EUR{$1}', + meta: 'eurosym-cmd', + score: 3.661595357097087e-5 + } + ], + caption2: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'caption2-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'caption2-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'caption2-cmd', + score: 0.0003890810058478364 + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'caption2-cmd', + score: 0.0004717618449370015 + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'caption2-cmd', + score: 5.0133404990680195e-5 + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'caption2-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'caption2-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'caption2-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption2-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption2-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption2-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'caption2-cmd', + score: 0.00015256647321237863 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption2-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'caption2-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'caption2-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'caption2-cmd', + score: 0.021473212893597875 + } + ], + amsbsy: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'amsbsy-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'amsbsy-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'amsbsy-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsbsy-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsbsy-cmd', + score: 0.0063276692758974925 + } + ], + CJK: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'CJK-cmd', + score: 0.04598628699063736 + } + ], + makecell: [ + { + caption: '\\diaghead{}{}{}', + snippet: '\\diaghead{$1}{$2}{$3}', + meta: 'makecell-cmd', + score: 2.0417817976377812e-5 + }, + { + caption: '\\makecell{}', + snippet: '\\makecell{$1}', + meta: 'makecell-cmd', + score: 0.005023670619810683 + }, + { + caption: '\\makecell[]{}', + snippet: '\\makecell[$1]{$2}', + meta: 'makecell-cmd', + score: 0.005023670619810683 + }, + { + caption: '\\height', + snippet: '\\height', + meta: 'makecell-cmd', + score: 0.0045883162478394055 + }, + { + caption: '\\height{}', + snippet: '\\height{$1}', + meta: 'makecell-cmd', + score: 0.0045883162478394055 + }, + { + caption: '\\setcellgapes{}', + snippet: '\\setcellgapes{$1}', + meta: 'makecell-cmd', + score: 0.0004960838428758984 + }, + { + caption: '\\thead{}', + snippet: '\\thead{$1}', + meta: 'makecell-cmd', + score: 0.0023087638254186797 + }, + { + caption: '\\Gape[]', + snippet: '\\Gape[$1]', + meta: 'makecell-cmd', + score: 0.000469300371741866 + }, + { + caption: '\\theadgape{}', + snippet: '\\theadgape{$1}', + meta: 'makecell-cmd', + score: 0.000234650185870933 + }, + { + caption: '\\theadalign', + snippet: '\\theadalign', + meta: 'makecell-cmd', + score: 0.0006746935448099005 + }, + { + caption: '\\theadalign{}', + snippet: '\\theadalign{$1}', + meta: 'makecell-cmd', + score: 0.0006746935448099005 + }, + { + caption: '\\theadset{}', + snippet: '\\theadset{$1}', + meta: 'makecell-cmd', + score: 0.0004400433589389675 + }, + { + caption: '\\Xhline{}', + snippet: '\\Xhline{$1}', + meta: 'makecell-cmd', + score: 0.0024175651338281096 + }, + { + caption: '\\theadfont{}', + snippet: '\\theadfont{$1}', + meta: 'makecell-cmd', + score: 0.0007935193556772338 + }, + { + caption: '\\theadfont', + snippet: '\\theadfont', + meta: 'makecell-cmd', + score: 0.0007935193556772338 + }, + { + caption: '\\cellgape{}', + snippet: '\\cellgape{$1}', + meta: 'makecell-cmd', + score: 0.000234650185870933 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'makecell-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'makecell-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\makegapedcells', + snippet: '\\makegapedcells', + meta: 'makecell-cmd', + score: 0.000431467454221244 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'makecell-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'makecell-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'makecell-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'makecell-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'makecell-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'makecell-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'makecell-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'makecell-cmd', + score: 0.018615449342361392 + } + ], + xeCJK: [ + { + caption: '\\setCJKmonofont{}', + snippet: '\\setCJKmonofont{$1}', + meta: 'xeCJK-cmd', + score: 0.0057178353252375245 + }, + { + caption: '\\setCJKmainfont{}', + snippet: '\\setCJKmainfont{$1}', + meta: 'xeCJK-cmd', + score: 0.006622926778590894 + }, + { + caption: '\\setCJKmainfont[]{}', + snippet: '\\setCJKmainfont[$1]{$2}', + meta: 'xeCJK-cmd', + score: 0.006622926778590894 + }, + { + caption: '\\setCJKsansfont{}', + snippet: '\\setCJKsansfont{$1}', + meta: 'xeCJK-cmd', + score: 0.0057178353252375245 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xeCJK-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xeCJK-cmd', + score: 0.2864294797053033 + } + ], + threeparttable: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'threeparttable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'threeparttable-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'threeparttable-cmd', + score: 3.800886892251021 + } + ], + dirtytalk: [ + { + caption: '\\say{}', + snippet: '\\say{$1}', + meta: 'dirtytalk-cmd', + score: 0.010246289746417045 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'dirtytalk-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'dirtytalk-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dirtytalk-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'dirtytalk-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'dirtytalk-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'dirtytalk-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'dirtytalk-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'dirtytalk-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'dirtytalk-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'dirtytalk-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'dirtytalk-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dirtytalk-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dirtytalk-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'dirtytalk-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'dirtytalk-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'dirtytalk-cmd', + score: 0.021170869458413965 + } + ], + balance: [ + { + caption: '\\balance', + snippet: '\\balance', + meta: 'balance-cmd', + score: 0.003629066156300264 + }, + { + caption: '\\balance{}', + snippet: '\\balance{$1}', + meta: 'balance-cmd', + score: 0.003629066156300264 + } + ], + minted: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\usemintedstyle{}', + snippet: '\\usemintedstyle{$1}', + meta: 'minted-cmd', + score: 0.00184279823796158 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\inputminted[]{}{}', + snippet: '\\inputminted[$1]{$2}{$3}', + meta: 'minted-cmd', + score: 0.0016501519191680601 + }, + { + caption: '\\inputminted{}{}', + snippet: '\\inputminted{$1}{$2}', + meta: 'minted-cmd', + score: 0.0016501519191680601 + }, + { + caption: '\\setminted[]{}', + snippet: '\\setminted[$1]{$2}', + meta: 'minted-cmd', + score: 0.0004017914210172805 + }, + { + caption: '\\setminted{}', + snippet: '\\setminted{$1}', + meta: 'minted-cmd', + score: 0.0004017914210172805 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'minted-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'minted-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'minted-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'minted-cmd', + score: 0.0009837365348002915 + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'minted-cmd', + score: 0.0005815474978918903 + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'minted-cmd', + score: 0.0008866338267686714 + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'minted-cmd', + score: 0.0015470917047414941 + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'minted-cmd', + score: 0.0011934321931750752 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'minted-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'minted-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'minted-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'minted-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'minted-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'minted-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'minted-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'minted-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'minted-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'minted-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'minted-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'minted-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'minted-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'minted-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'minted-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fbox{}', + snippet: '\\fbox{$1}', + meta: 'minted-cmd', + score: 0.020865450075016792 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\pagewiselinenumbers', + snippet: '\\pagewiselinenumbers', + meta: 'minted-cmd', + score: 0.00016870831850106035 + }, + { + caption: '\\linenomath', + snippet: '\\linenomath', + meta: 'minted-cmd', + score: 1.4517338420208715e-5 + }, + { + caption: '\\linenumberfont{}', + snippet: '\\linenumberfont{$1}', + meta: 'minted-cmd', + score: 0.0001811784338695797 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\endlinenomath', + snippet: '\\endlinenomath', + meta: 'minted-cmd', + score: 1.4517338420208715e-5 + }, + { + caption: '\\nolinenumbers', + snippet: '\\nolinenumbers', + meta: 'minted-cmd', + score: 0.0009805246614299932 + }, + { + caption: '\\path', + snippet: '\\path', + meta: 'minted-cmd', + score: 0.028200474217322108 + }, + { + caption: '\\path[]', + snippet: '\\path[$1]', + meta: 'minted-cmd', + score: 0.028200474217322108 + }, + { + caption: '\\path{}', + snippet: '\\path{$1}', + meta: 'minted-cmd', + score: 0.028200474217322108 + }, + { + caption: '\\filedate{}', + snippet: '\\filedate{$1}', + meta: 'minted-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\filedate', + snippet: '\\filedate', + meta: 'minted-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\linenumbers', + snippet: '\\linenumbers', + meta: 'minted-cmd', + score: 0.004687680659497865 + }, + { + caption: '\\modulolinenumbers[]', + snippet: '\\modulolinenumbers[$1]', + meta: 'minted-cmd', + score: 0.0027194991933605197 + }, + { + caption: '\\fileversion{}', + snippet: '\\fileversion{$1}', + meta: 'minted-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\fileversion', + snippet: '\\fileversion', + meta: 'minted-cmd', + score: 0.000578146635331119 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'minted-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'minted-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\fvset{}', + snippet: '\\fvset{$1}', + meta: 'minted-cmd', + score: 0.00015476887282479622 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'minted-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'minted-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'minted-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'minted-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'minted-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'minted-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'minted-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'minted-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'minted-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'minted-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'minted-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'minted-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'minted-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157 + } + ], + xifthen: [ + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xifthen-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xifthen-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xifthen-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xifthen-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xifthen-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'xifthen-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xifthen-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'xifthen-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'xifthen-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xifthen-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xifthen-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'xifthen-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'xifthen-cmd', + score: 0.028955796305270766 + } + ], + relsize: [ + { + caption: '\\mathlarger{}', + snippet: '\\mathlarger{$1}', + meta: 'relsize-cmd', + score: 0.0031475241540308316 + }, + { + caption: '\\smaller', + snippet: '\\smaller', + meta: 'relsize-cmd', + score: 0.001271007880944704 + } + ], + epsf: [ + { + caption: '\\epsfbox{}', + snippet: '\\epsfbox{$1}', + meta: 'epsf-cmd', + score: 0.00013712781345832882 + } + ], + datetime: [ + { + caption: '\\shortmonthname[]', + snippet: '\\shortmonthname[$1]', + meta: 'datetime-cmd', + score: 0.00018524143860552933 + }, + { + caption: '\\THEYEAR', + snippet: '\\THEYEAR', + meta: 'datetime-cmd', + score: 8.638115929876123e-5 + }, + { + caption: '\\currenttime', + snippet: '\\currenttime', + meta: 'datetime-cmd', + score: 0.0002884868472087627 + }, + { + caption: '\\monthname', + snippet: '\\monthname', + meta: 'datetime-cmd', + score: 8.847106423071211e-5 + }, + { + caption: '\\monthname[]', + snippet: '\\monthname[$1]', + meta: 'datetime-cmd', + score: 8.847106423071211e-5 + }, + { + caption: '\\today', + snippet: '\\today', + meta: 'datetime-cmd', + score: 0.10733849317324783 + }, + { + caption: '\\THEMONTH', + snippet: '\\THEMONTH', + meta: 'datetime-cmd', + score: 8.638115929876123e-5 + }, + { + caption: '\\yyyymmdddate', + snippet: '\\yyyymmdddate', + meta: 'datetime-cmd', + score: 0.0002568405365040184 + }, + { + caption: '\\pdfdate', + snippet: '\\pdfdate', + meta: 'datetime-cmd', + score: 9.673490669434574e-5 + }, + { + caption: '\\dateseparator', + snippet: '\\dateseparator', + meta: 'datetime-cmd', + score: 0.00010966778823652713 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\THEDAY', + snippet: '\\THEDAY', + meta: 'datetime-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\usdate', + snippet: '\\usdate', + meta: 'datetime-cmd', + score: 0.00020980148911330757 + }, + { + caption: '\\newdateformat{}{}', + snippet: '\\newdateformat{$1}{$2}', + meta: 'datetime-cmd', + score: 8.638115929876123e-5 + }, + { + caption: '\\settimeformat{}', + snippet: '\\settimeformat{$1}', + meta: 'datetime-cmd', + score: 0.00010966778823652713 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'datetime-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'datetime-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'datetime-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'datetime-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'datetime-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'datetime-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'datetime-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'datetime-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'datetime-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'datetime-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'datetime-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datetime-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'datetime-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'datetime-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'datetime-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'datetime-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'datetime-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'datetime-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datetime-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'datetime-cmd', + score: 0.0063276692758974925 + } + ], + fontawesome: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'fontawesome-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fontawesome-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'fontawesome-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'fontawesome-cmd', + score: 0.2864294797053033 + } + ], + forest: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\bracketset{}', + snippet: '\\bracketset{$1}', + meta: 'forest-cmd', + score: 0.00014301574866674164 + }, + { + caption: '\\forestset{}', + snippet: '\\forestset{$1}', + meta: 'forest-cmd', + score: 0.0020596473883671114 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'forest-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'forest-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'forest-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'forest-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'forest-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'forest-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'forest-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'forest-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'forest-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'forest-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'forest-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'forest-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'forest-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'forest-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'forest-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'forest-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'forest-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'forest-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'forest-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'forest-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'forest-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'forest-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'forest-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'forest-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'forest-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'forest-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'forest-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'forest-cmd', + score: 0.0018653410309739879 + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'forest-cmd', + score: 0.00031058155311734754 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'forest-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'forest-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'forest-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'forest-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'forest-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'forest-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'forest-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'forest-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'forest-cmd', + score: 0.2864294797053033 + } + ], + pgf: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgf-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgf-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgf-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgf-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgf-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgf-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgf-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgf-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgf-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgf-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgf-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgf-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgf-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgf-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgf-cmd', + score: 0.2864294797053033 + } + ], + pstricks: [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pstricks-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pstricks-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pstricks-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pstricks-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pstricks-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pstricks-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pstricks-cmd', + score: 0.006520475264573554 + } + ], + fancybox: [ + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'fancybox-cmd', + score: 0.00107667147399019 + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'fancybox-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'fancybox-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'fancybox-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'fancybox-cmd', + score: 4.5350034239275855e-5 + } + ], + braket: [ + { + caption: '\\ket{}', + snippet: '\\ket{$1}', + meta: 'braket-cmd', + score: 0.0326276280979336 + }, + { + caption: '\\braket{}{}', + snippet: '\\braket{$1}{$2}', + meta: 'braket-cmd', + score: 0.004421747491186916 + }, + { + caption: '\\braket{}', + snippet: '\\braket{$1}', + meta: 'braket-cmd', + score: 0.004421747491186916 + }, + { + caption: '\\ketbra{}{}', + snippet: '\\ketbra{$1}{$2}', + meta: 'braket-cmd', + score: 0.0006317858348936015 + }, + { + caption: '\\ketbra', + snippet: '\\ketbra', + meta: 'braket-cmd', + score: 0.0006317858348936015 + }, + { + caption: '\\bra{}', + snippet: '\\bra{$1}', + meta: 'braket-cmd', + score: 0.005609763332417241 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'braket-cmd', + score: 0.008565354665444157 + } + ], + import: [ + { + caption: '\\import{}{}', + snippet: '\\import{$1}{$2}', + meta: 'import-cmd', + score: 0.1265354812350108 + } + ], + abntex2cite: [ + { + caption: '\\citeonline{}', + snippet: '\\citeonline{$1}', + meta: 'abntex2cite-cmd', + score: 0.014277840409455324 + }, + { + caption: '\\bibitem{}', + snippet: '\\bibitem{$1}', + meta: 'abntex2cite-cmd', + score: 0.3689547570562042 + }, + { + caption: '\\bibitem[]{}', + snippet: '\\bibitem[$1]{$2}', + meta: 'abntex2cite-cmd', + score: 0.3689547570562042 + }, + { + caption: '\\bibliographystyle{}', + snippet: '\\bibliographystyle{$1}', + meta: 'abntex2cite-cmd', + score: 0.25122317941387773 + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'abntex2cite-cmd', + score: 0.01091041305836494 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'abntex2cite-cmd', + score: 2.341195220791228 + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'abntex2cite-cmd', + score: 0.2659628337907604 + }, + { + caption: '\\setstretch{}', + snippet: '\\setstretch{$1}', + meta: 'abntex2cite-cmd', + score: 0.019634763572332112 + }, + { + caption: '\\onehalfspacing', + snippet: '\\onehalfspacing', + meta: 'abntex2cite-cmd', + score: 0.010655415521079565 + }, + { + caption: '\\singlespacing', + snippet: '\\singlespacing', + meta: 'abntex2cite-cmd', + score: 0.008351544612280968 + }, + { + caption: '\\doublespacing', + snippet: '\\doublespacing', + meta: 'abntex2cite-cmd', + score: 0.007835428951987135 + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'abntex2cite-cmd', + score: 0.03225350148161425 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'abntex2cite-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'abntex2cite-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'abntex2cite-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'abntex2cite-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'abntex2cite-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'abntex2cite-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'abntex2cite-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'abntex2cite-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'abntex2cite-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'abntex2cite-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'abntex2cite-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'abntex2cite-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'abntex2cite-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'abntex2cite-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'abntex2cite-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'abntex2cite-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'abntex2cite-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'abntex2cite-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'abntex2cite-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'abntex2cite-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'abntex2cite-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'abntex2cite-cmd', + score: 0.0018957469739775527 + } + ], + isodate: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'isodate-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'isodate-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'isodate-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'isodate-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'isodate-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'isodate-cmd', + score: 0.0018957469739775527 + } + ], + tcolorbox: [ + { + caption: '\\tcbset{}', + snippet: '\\tcbset{$1}', + meta: 'tcolorbox-cmd', + score: 0.00012246447222402193 + }, + { + caption: '\\tcbuselibrary{}', + snippet: '\\tcbuselibrary{$1}', + meta: 'tcolorbox-cmd', + score: 4.347671035621014e-5 + }, + { + caption: '\\newtcolorbox[]{}[][]{}', + snippet: '\\newtcolorbox[$1]{$2}[$3][$4]{$5}', + meta: 'tcolorbox-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'tcolorbox-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'tcolorbox-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\newtcbox{}[][]{}', + snippet: '\\newtcbox{$1}[$2][$3]{$4}', + meta: 'tcolorbox-cmd', + score: 3.558785984219631e-5 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'tcolorbox-cmd', + score: 0.0022216421267780076 + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'tcolorbox-cmd', + score: 0.0072203369120285256 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'tcolorbox-cmd', + score: 0.413853376001159 + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'tcolorbox-cmd', + score: 0.0024547099784948665 + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'tcolorbox-cmd', + score: 0.0024547099784948665 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tcolorbox-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tcolorbox-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tcolorbox-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tcolorbox-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tcolorbox-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tcolorbox-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tcolorbox-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tcolorbox-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tcolorbox-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tcolorbox-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tcolorbox-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tcolorbox-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tcolorbox-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tcolorbox-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tcolorbox-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tcolorbox-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tcolorbox-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tcolorbox-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tcolorbox-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tcolorbox-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tcolorbox-cmd', + score: 0.2864294797053033 + } + ], + vmargin: [ + { + caption: '\\setmargins{}', + snippet: '\\setmargins{$1}', + meta: 'vmargin-cmd', + score: 3.138510306083217e-5 + }, + { + caption: '\\setmarginsrb{}{}{}{}{}{}{}{}', + snippet: '\\setmarginsrb{$1}{$2}{$3}{$4}{$5}{$6}{$7}{$8}', + meta: 'vmargin-cmd', + score: 0.0004759508676929243 + }, + { + caption: '\\setpapersize{}', + snippet: '\\setpapersize{$1}', + meta: 'vmargin-cmd', + score: 3.138510306083217e-5 + } + ], + mdframed: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newmdenv[]{}', + snippet: '\\newmdenv[$1]{$2}', + meta: 'mdframed-cmd', + score: 0.0008776774843208122 + }, + { + caption: '\\surroundwithmdframed[]{}', + snippet: '\\surroundwithmdframed[$1]{$2}', + meta: 'mdframed-cmd', + score: 5.535446508489438e-5 + }, + { + caption: '\\newmdtheoremenv{}{}', + snippet: '\\newmdtheoremenv{$1}{$2}', + meta: 'mdframed-cmd', + score: 3.558785984219631e-5 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mdframed-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mdframed-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'mdframed-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'mdframed-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'mdframed-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mdframed-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mdframed-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mdframed-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mdframed-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'mdframed-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'mdframed-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mdframed-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mdframed-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'mdframed-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mdframed-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mdframed-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'mdframed-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'mdframed-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'mdframed-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'mdframed-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'mdframed-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'mdframed-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'mdframed-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157 + } + ], + cancel: [ + { + caption: '\\cancel{}', + snippet: '\\cancel{$1}', + meta: 'cancel-cmd', + score: 0.00017782514657538044 + }, + { + caption: '\\cancelto{}{}', + snippet: '\\cancelto{$1}{$2}', + meta: 'cancel-cmd', + score: 7.809089624140706e-5 + } + ], + textcase: [ + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'textcase-cmd', + score: 2.341195220791228 + } + ], + libertine: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'libertine-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'libertine-cmd', + score: 0.008565354665444157 + } + ], + flushend: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'flushend-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'flushend-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'flushend-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'flushend-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'flushend-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'flushend-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'flushend-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'flushend-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'flushend-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'flushend-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'flushend-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'flushend-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'flushend-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flushend-cmd', + score: 0.008565354665444157 + } + ], + psfrag: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psfrag-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'psfrag-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'psfrag-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'psfrag-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'psfrag-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'psfrag-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'psfrag-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'psfrag-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'psfrag-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'psfrag-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psfrag-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'psfrag-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'psfrag-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'psfrag-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'psfrag-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'psfrag-cmd', + score: 0.004649150613625593 + } + ], + tablefootnote: [ + { + caption: '\\tablefootnote{}', + snippet: '\\tablefootnote{$1}', + meta: 'tablefootnote-cmd', + score: 0.00017554048326570823 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'tablefootnote-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'tablefootnote-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'tablefootnote-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tablefootnote-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tablefootnote-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tablefootnote-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tablefootnote-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tablefootnote-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tablefootnote-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tablefootnote-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965 + } + ], + amstext: [ + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'amstext-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'amstext-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'amstext-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'amstext-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amstext-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amstext-cmd', + score: 0.0063276692758974925 + } + ], + units: [ + { + caption: '\\unitfrac{}{}', + snippet: '\\unitfrac{$1}{$2}', + meta: 'units-cmd', + score: 0.0009264866770139672 + }, + { + caption: '\\unitfrac[]{}{}', + snippet: '\\unitfrac[$1]{$2}{$3}', + meta: 'units-cmd', + score: 0.0009264866770139672 + }, + { + caption: '\\unit[]{}', + snippet: '\\unit[$1]{$2}', + meta: 'units-cmd', + score: 0.028299796173135428 + }, + { + caption: '\\unit{}', + snippet: '\\unit{$1}', + meta: 'units-cmd', + score: 0.028299796173135428 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'units-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'units-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'units-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'units-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'units-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'units-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\nicefrac{}{}', + snippet: '\\nicefrac{$1}{$2}', + meta: 'units-cmd', + score: 0.0018011350423659288 + } + ], + scrextend: [ + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'scrextend-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'scrextend-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\scriptsize', + snippet: '\\scriptsize', + meta: 'scrextend-cmd', + score: 0.05550618634921613 + }, + { + caption: '\\scriptsize{}', + snippet: '\\scriptsize{$1}', + meta: 'scrextend-cmd', + score: 0.05550618634921613 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'scrextend-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\Large', + snippet: '\\Large', + meta: 'scrextend-cmd', + score: 0.1987771081149759 + }, + { + caption: '\\Large{}', + snippet: '\\Large{$1}', + meta: 'scrextend-cmd', + score: 0.1987771081149759 + }, + { + caption: '\\and', + snippet: '\\and', + meta: 'scrextend-cmd', + score: 0.09847866956528724 + }, + { + caption: '\\LARGE', + snippet: '\\LARGE', + meta: 'scrextend-cmd', + score: 0.05947642043953873 + }, + { + caption: '\\LARGE{}', + snippet: '\\LARGE{$1}', + meta: 'scrextend-cmd', + score: 0.05947642043953873 + }, + { + caption: '\\subtitle{}', + snippet: '\\subtitle{$1}', + meta: 'scrextend-cmd', + score: 0.01803265454797817 + }, + { + caption: '\\large', + snippet: '\\large', + meta: 'scrextend-cmd', + score: 0.20377416734108866 + }, + { + caption: '\\large{}', + snippet: '\\large{$1}', + meta: 'scrextend-cmd', + score: 0.20377416734108866 + }, + { + caption: '\\Huge', + snippet: '\\Huge', + meta: 'scrextend-cmd', + score: 0.04725806985998919 + }, + { + caption: '\\footnotesize', + snippet: '\\footnotesize', + meta: 'scrextend-cmd', + score: 0.2038592081252624 + }, + { + caption: '\\footnotesize{}', + snippet: '\\footnotesize{$1}', + meta: 'scrextend-cmd', + score: 0.2038592081252624 + }, + { + caption: '\\small', + snippet: '\\small', + meta: 'scrextend-cmd', + score: 0.2447632045426295 + }, + { + caption: '\\small{}', + snippet: '\\small{$1}', + meta: 'scrextend-cmd', + score: 0.2447632045426295 + }, + { + caption: '\\huge', + snippet: '\\huge', + meta: 'scrextend-cmd', + score: 0.04229832859754922 + }, + { + caption: '\\huge{}', + snippet: '\\huge{$1}', + meta: 'scrextend-cmd', + score: 0.04229832859754922 + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'scrextend-cmd', + score: 0.044016804142963585 + }, + { + caption: '\\tiny{}', + snippet: '\\tiny{$1}', + meta: 'scrextend-cmd', + score: 0.047727606910742924 + }, + { + caption: '\\tiny', + snippet: '\\tiny', + meta: 'scrextend-cmd', + score: 0.047727606910742924 + }, + { + caption: '\\deffootnote[]{}{}{}', + snippet: '\\deffootnote[$1]{$2}{$3}{$4}', + meta: 'scrextend-cmd', + score: 2.545393270896533e-5 + }, + { + caption: '\\thefootnote', + snippet: '\\thefootnote', + meta: 'scrextend-cmd', + score: 0.007676927812687567 + }, + { + caption: '\\thefootnote{}', + snippet: '\\thefootnote{$1}', + meta: 'scrextend-cmd', + score: 0.007676927812687567 + }, + { + caption: '\\normalsize', + snippet: '\\normalsize', + meta: 'scrextend-cmd', + score: 0.14261697855738878 + }, + { + caption: '\\normalsize{}', + snippet: '\\normalsize{$1}', + meta: 'scrextend-cmd', + score: 0.14261697855738878 + }, + { + caption: '\\titlefont', + snippet: '\\titlefont', + meta: 'scrextend-cmd', + score: 0.0005278519180709353 + }, + { + caption: '\\thefootnotemark', + snippet: '\\thefootnotemark', + meta: 'scrextend-cmd', + score: 2.545393270896533e-5 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrextend-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrextend-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrextend-cmd', + score: 0.0008555564394100388 + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrextend-cmd', + score: 0.012985816912639263 + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrextend-cmd', + score: 0.000396664302361659 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrextend-cmd', + score: 0.00037306820619479756 + } + ], + mwe: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mwe-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mwe-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'mwe-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'mwe-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'mwe-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'mwe-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'mwe-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mwe-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'mwe-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mwe-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'mwe-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'mwe-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'mwe-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'mwe-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'mwe-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'mwe-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'mwe-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'mwe-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'mwe-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mwe-cmd', + score: 0.008565354665444157 + } + ], + beamerposter: [ + { + caption: '\\scriptsize', + snippet: '\\scriptsize', + meta: 'beamerposter-cmd', + score: 0.05550618634921613 + }, + { + caption: '\\scriptsize{}', + snippet: '\\scriptsize{$1}', + meta: 'beamerposter-cmd', + score: 0.05550618634921613 + }, + { + caption: '\\Large', + snippet: '\\Large', + meta: 'beamerposter-cmd', + score: 0.1987771081149759 + }, + { + caption: '\\Large{}', + snippet: '\\Large{$1}', + meta: 'beamerposter-cmd', + score: 0.1987771081149759 + }, + { + caption: '\\footnotesize', + snippet: '\\footnotesize', + meta: 'beamerposter-cmd', + score: 0.2038592081252624 + }, + { + caption: '\\footnotesize{}', + snippet: '\\footnotesize{$1}', + meta: 'beamerposter-cmd', + score: 0.2038592081252624 + }, + { + caption: '\\LARGE', + snippet: '\\LARGE', + meta: 'beamerposter-cmd', + score: 0.05947642043953873 + }, + { + caption: '\\LARGE{}', + snippet: '\\LARGE{$1}', + meta: 'beamerposter-cmd', + score: 0.05947642043953873 + }, + { + caption: '\\large', + snippet: '\\large', + meta: 'beamerposter-cmd', + score: 0.20377416734108866 + }, + { + caption: '\\large{}', + snippet: '\\large{$1}', + meta: 'beamerposter-cmd', + score: 0.20377416734108866 + }, + { + caption: '\\VeryHuge', + snippet: '\\VeryHuge', + meta: 'beamerposter-cmd', + score: 0.000892251826639951 + }, + { + caption: '\\small', + snippet: '\\small', + meta: 'beamerposter-cmd', + score: 0.2447632045426295 + }, + { + caption: '\\small{}', + snippet: '\\small{$1}', + meta: 'beamerposter-cmd', + score: 0.2447632045426295 + }, + { + caption: '\\VERYHuge', + snippet: '\\VERYHuge', + meta: 'beamerposter-cmd', + score: 0.0011668714784222325 + }, + { + caption: '\\veryHuge', + snippet: '\\veryHuge', + meta: 'beamerposter-cmd', + score: 0.000892251826639951 + }, + { + caption: '\\normalsize', + snippet: '\\normalsize', + meta: 'beamerposter-cmd', + score: 0.14261697855738878 + }, + { + caption: '\\normalsize{}', + snippet: '\\normalsize{$1}', + meta: 'beamerposter-cmd', + score: 0.14261697855738878 + }, + { + caption: '\\tiny{}', + snippet: '\\tiny{$1}', + meta: 'beamerposter-cmd', + score: 0.047727606910742924 + }, + { + caption: '\\tiny', + snippet: '\\tiny', + meta: 'beamerposter-cmd', + score: 0.047727606910742924 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'beamerposter-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'beamerposter-cmd', + score: 0.021170869458413965 + } + ], + footnote: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'footnote-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'footnote-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\makesavenoteenv{}', + snippet: '\\makesavenoteenv{$1}', + meta: 'footnote-cmd', + score: 0.0018587414325895479 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'footnote-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'footnote-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\parbox{}{}', + snippet: '\\parbox{$1}{$2}', + meta: 'footnote-cmd', + score: 0.04800611019618169 + } + ], + invoice: [ + { + caption: '\\Fee{}{}{}', + snippet: '\\Fee{$1}{$2}{$3}', + meta: 'invoice-cmd', + score: 0.003295435821387378 + }, + { + caption: '\\ProjectTitle{}', + snippet: '\\ProjectTitle{$1}', + meta: 'invoice-cmd', + score: 0.003295435821387378 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'invoice-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'invoice-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'invoice-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'invoice-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'invoice-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'invoice-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'invoice-cmd', + score: 0.0023853501147448834 + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'invoice-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'invoice-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'invoice-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'invoice-cmd', + score: 9.952664522415981e-5 + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'invoice-cmd', + score: 0.0016148498709822416 + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'invoice-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'invoice-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'invoice-cmd', + score: 0.0029238994233674776 + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'invoice-cmd', + score: 0.0313525090421608 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'invoice-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'invoice-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'invoice-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'invoice-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'invoice-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'invoice-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'invoice-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'invoice-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'invoice-cmd', + score: 0.028955796305270766 + } + ], + tikzpeople: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'tikzpeople-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'tikzpeople-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'tikzpeople-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikzpeople-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikzpeople-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikzpeople-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpeople-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikzpeople-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikzpeople-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tikzpeople-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tikzpeople-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tikzpeople-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tikzpeople-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzpeople-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tikzpeople-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzpeople-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpeople-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tikzpeople-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikzpeople-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikzpeople-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpeople-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikzpeople-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikzpeople-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikzpeople-cmd', + score: 0.2864294797053033 + } + ], + titletoc: [ + { + caption: '\\thecontentspage', + snippet: '\\thecontentspage', + meta: 'titletoc-cmd', + score: 0.0008054115902675176 + }, + { + caption: '\\startcontents', + snippet: '\\startcontents', + meta: 'titletoc-cmd', + score: 0.00026847053008917257 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'titletoc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'titletoc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\printcontents{}{}{}', + snippet: '\\printcontents{$1}{$2}{$3}', + meta: 'titletoc-cmd', + score: 0.00013423526504458629 + }, + { + caption: '\\titlecontents{}[]', + snippet: '\\titlecontents{$1}[$2]', + meta: 'titletoc-cmd', + score: 0.0017036290423289926 + }, + { + caption: '\\titlecontents{}[]{}{}{}{}[]', + snippet: '\\titlecontents{$1}[$2]{$3}{$4}{$5}{$6}[$7]', + meta: 'titletoc-cmd', + score: 0.0017036290423289926 + }, + { + caption: '\\titlecontents{}[]{}{}{}{}', + snippet: '\\titlecontents{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'titletoc-cmd', + score: 0.0017036290423289926 + }, + { + caption: '\\numberline{}', + snippet: '\\numberline{$1}', + meta: 'titletoc-cmd', + score: 0.007461440567272885 + }, + { + caption: '\\dottedcontents{}[]{}{}{}', + snippet: '\\dottedcontents{$1}[$2]{$3}{$4}{$5}', + meta: 'titletoc-cmd', + score: 4.743909531747666e-5 + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'titletoc-cmd', + score: 0.0004835660211260246 + }, + { + caption: '\\thecontentslabel', + snippet: '\\thecontentslabel', + meta: 'titletoc-cmd', + score: 0.0010521864830662522 + }, + { + caption: '\\contentsuse{}{}', + snippet: '\\contentsuse{$1}{$2}', + meta: 'titletoc-cmd', + score: 6.110202388233705e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'titletoc-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\contentspage', + snippet: '\\contentspage', + meta: 'titletoc-cmd', + score: 0.0004955116569277163 + }, + { + caption: '\\contentslabel[]{}', + snippet: '\\contentslabel[$1]{$2}', + meta: 'titletoc-cmd', + score: 0.0011055859582683105 + }, + { + caption: '\\contentslabel{}', + snippet: '\\contentslabel{$1}', + meta: 'titletoc-cmd', + score: 0.0011055859582683105 + }, + { + caption: '\\contentsmargin{}', + snippet: '\\contentsmargin{$1}', + meta: 'titletoc-cmd', + score: 0.00013423526504458629 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'titletoc-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'titletoc-cmd', + score: 0.019273712561461216 + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'titletoc-cmd', + score: 0.019273712561461216 + } + ], + dblfloatfix: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'dblfloatfix-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'dblfloatfix-cmd', + score: 0.354445763583904 + }, + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'dblfloatfix-cmd', + score: 0.058405875394131175 + }, + { + caption: '\\em', + snippet: '\\em', + meta: 'dblfloatfix-cmd', + score: 0.10357353994640862 + } + ], + pgfplotstable: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'pgfplotstable-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'pgfplotstable-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'pgfplotstable-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'pgfplotstable-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfplotstable-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfplotstable-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplotstable-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfplotstable-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfplotstable-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfplotstable-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplotstable-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfplotstable-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfplotstable-cmd', + score: 0.2864294797053033 + } + ], + acronym: [ + { + caption: '\\acp{}', + snippet: '\\acp{$1}', + meta: 'acronym-cmd', + score: 0.0005185177930914685 + }, + { + caption: '\\acsfont{}', + snippet: '\\acsfont{$1}', + meta: 'acronym-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\aclabelfont', + snippet: '\\aclabelfont', + meta: 'acronym-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\acro{}{}', + snippet: '\\acro{$1}{$2}', + meta: 'acronym-cmd', + score: 0.023587207425038587 + }, + { + caption: '\\acl{}', + snippet: '\\acl{$1}', + meta: 'acronym-cmd', + score: 0.0008131607751426444 + }, + { + caption: '\\acf{}', + snippet: '\\acf{$1}', + meta: 'acronym-cmd', + score: 0.0006845634165950408 + }, + { + caption: '\\acrodef{}[]{}', + snippet: '\\acrodef{$1}[$2]{$3}', + meta: 'acronym-cmd', + score: 0.0002902047200830372 + }, + { + caption: '\\acs{}', + snippet: '\\acs{$1}', + meta: 'acronym-cmd', + score: 0.002351209826598939 + }, + { + caption: '\\acfp{}', + snippet: '\\acfp{$1}', + meta: 'acronym-cmd', + score: 2.2013599341265054e-5 + }, + { + caption: '\\ac{}', + snippet: '\\ac{$1}', + meta: 'acronym-cmd', + score: 0.04714113215364704 + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'acronym-cmd', + score: 0.03789745970461662 + } + ], + nicefrac: [ + { + caption: '\\nicefrac{}{}', + snippet: '\\nicefrac{$1}{$2}', + meta: 'nicefrac-cmd', + score: 0.0018011350423659288 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'nicefrac-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'nicefrac-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'nicefrac-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'nicefrac-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'nicefrac-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'nicefrac-cmd', + score: 0.0018957469739775527 + } + ], + smartdiagram: [ + { + caption: '\\usesmartdiagramlibrary{}', + snippet: '\\usesmartdiagramlibrary{$1}', + meta: 'smartdiagram-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'smartdiagram-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'smartdiagram-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'smartdiagram-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'smartdiagram-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'smartdiagram-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'smartdiagram-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'smartdiagram-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'smartdiagram-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'smartdiagram-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'smartdiagram-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'smartdiagram-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'smartdiagram-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'smartdiagram-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'smartdiagram-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'smartdiagram-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'smartdiagram-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'smartdiagram-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'smartdiagram-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'smartdiagram-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033 + } + ], + qtree: [ + { + caption: '\\qroof{}', + snippet: '\\qroof{$1}', + meta: 'qtree-cmd', + score: 0.00012663929287995903 + }, + { + caption: '\\Tree[]', + snippet: '\\Tree[$1]', + meta: 'qtree-cmd', + score: 0.0008894716589418522 + }, + { + caption: '\\Tree', + snippet: '\\Tree', + meta: 'qtree-cmd', + score: 0.0008894716589418522 + } + ], + backref: [ + { + caption: '\\backrefpagesname', + snippet: '\\backrefpagesname', + meta: 'backref-cmd', + score: 0.0022756001200686213 + }, + { + caption: '\\backref', + snippet: '\\backref', + meta: 'backref-cmd', + score: 0.0025820187198826706 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'backref-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'backref-cmd', + score: 0.006609629561859019 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'backref-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'backref-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'backref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'backref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'backref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'backref-cmd', + score: 0.010304996748556729 + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'backref-cmd', + score: 0.013774721817648336 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'backref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157 + } + ], + epigraph: [ + { + caption: '\\epigraphflush{}', + snippet: '\\epigraphflush{$1}', + meta: 'epigraph-cmd', + score: 1.8073688234300064e-5 + }, + { + caption: '\\epigraphsize{}', + snippet: '\\epigraphsize{$1}', + meta: 'epigraph-cmd', + score: 6.820709322498027e-5 + }, + { + caption: '\\epigraphsize', + snippet: '\\epigraphsize', + meta: 'epigraph-cmd', + score: 6.820709322498027e-5 + }, + { + caption: '\\epigraph{}{}', + snippet: '\\epigraph{$1}{$2}', + meta: 'epigraph-cmd', + score: 0.0031428856022970054 + } + ], + chngcntr: [ + { + caption: '\\counterwithin{}{}', + snippet: '\\counterwithin{$1}{$2}', + meta: 'chngcntr-cmd', + score: 0.001287401394784382 + }, + { + caption: '\\counterwithout{}{}', + snippet: '\\counterwithout{$1}{$2}', + meta: 'chngcntr-cmd', + score: 0.0026127666246546326 + } + ], + empheq: [ + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'empheq-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'empheq-cmd', + score: 1.897791904799601 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'empheq-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'empheq-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'empheq-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'empheq-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'empheq-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'empheq-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'empheq-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'empheq-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'empheq-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'empheq-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'empheq-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'empheq-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'empheq-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'empheq-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'empheq-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'empheq-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'empheq-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'empheq-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'empheq-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'empheq-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'empheq-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'empheq-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'empheq-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'empheq-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'empheq-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'empheq-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'empheq-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'empheq-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'empheq-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'empheq-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'empheq-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'empheq-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'empheq-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'empheq-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'empheq-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'empheq-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'empheq-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'empheq-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'empheq-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'empheq-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'empheq-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'empheq-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'empheq-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'empheq-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'empheq-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'empheq-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'empheq-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'empheq-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'empheq-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'empheq-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'empheq-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'empheq-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'empheq-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'empheq-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'empheq-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'empheq-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'empheq-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'empheq-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'empheq-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'empheq-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'empheq-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'empheq-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'empheq-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'empheq-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'empheq-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'empheq-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'empheq-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'empheq-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'empheq-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'empheq-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'empheq-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'empheq-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'empheq-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'empheq-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'empheq-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'empheq-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'empheq-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'empheq-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'empheq-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'empheq-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'empheq-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'empheq-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'empheq-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'empheq-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'empheq-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'empheq-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'empheq-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'empheq-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'empheq-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'empheq-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'empheq-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'empheq-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'empheq-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'empheq-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'empheq-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'empheq-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'empheq-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'empheq-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'empheq-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'empheq-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'empheq-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'empheq-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'empheq-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'empheq-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'empheq-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'empheq-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'empheq-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\xleftrightarrow[][]{}', + snippet: '\\xleftrightarrow[$1][$2]{$3}', + meta: 'empheq-cmd', + score: 4.015559489911509e-5 + }, + { + caption: '\\vcentcolon', + snippet: '\\vcentcolon', + meta: 'empheq-cmd', + score: 0.00021361943526711615 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'empheq-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\coloneqq', + snippet: '\\coloneqq', + meta: 'empheq-cmd', + score: 0.0014407293323958122 + }, + { + caption: '\\mathclap{}', + snippet: '\\mathclap{$1}', + meta: 'empheq-cmd', + score: 7.84378567451772e-5 + }, + { + caption: '\\adjustlimits', + snippet: '\\adjustlimits', + meta: 'empheq-cmd', + score: 0.0005307066890271085 + }, + { + caption: '\\MoveEqLeft', + snippet: '\\MoveEqLeft', + meta: 'empheq-cmd', + score: 5.343949980628182e-5 + }, + { + caption: '\\mathrlap{}', + snippet: '\\mathrlap{$1}', + meta: 'empheq-cmd', + score: 0.0003112817211637952 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'empheq-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\xhookrightarrow{}', + snippet: '\\xhookrightarrow{$1}', + meta: 'empheq-cmd', + score: 5.444260823474129e-5 + }, + { + caption: '\\DeclarePairedDelimiter{}{}{}', + snippet: '\\DeclarePairedDelimiter{$1}{$2}{$3}', + meta: 'empheq-cmd', + score: 0.0033916678416372487 + }, + { + caption: '\\DeclarePairedDelimiter', + snippet: '\\DeclarePairedDelimiter', + meta: 'empheq-cmd', + score: 0.0033916678416372487 + }, + { + caption: '\\prescript{}{}{}', + snippet: '\\prescript{$1}{$2}{$3}', + meta: 'empheq-cmd', + score: 8.833369785705982e-6 + }, + { + caption: '\\underbrace{}', + snippet: '\\underbrace{$1}', + meta: 'empheq-cmd', + score: 0.010373780436850907 + }, + { + caption: '\\mathllap{}', + snippet: '\\mathllap{$1}', + meta: 'empheq-cmd', + score: 3.140504277052775e-5 + }, + { + caption: '\\overbrace{}', + snippet: '\\overbrace{$1}', + meta: 'empheq-cmd', + score: 0.0006045704778718376 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'empheq-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'empheq-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'empheq-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'empheq-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'empheq-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'empheq-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'empheq-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'empheq-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'empheq-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'empheq-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'empheq-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'empheq-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'empheq-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'empheq-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'empheq-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'empheq-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'empheq-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'empheq-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'empheq-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'empheq-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'empheq-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'empheq-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'empheq-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'empheq-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'empheq-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'empheq-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'empheq-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'empheq-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'empheq-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'empheq-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'empheq-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'empheq-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'empheq-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'empheq-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'empheq-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'empheq-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'empheq-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'empheq-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'empheq-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'empheq-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'empheq-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'empheq-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'empheq-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'empheq-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'empheq-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'empheq-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'empheq-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'empheq-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'empheq-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'empheq-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'empheq-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'empheq-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'empheq-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'empheq-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'empheq-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'empheq-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'empheq-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'empheq-cmd', + score: 0.0063276692758974925 + } + ], + mathexam: [ + { + caption: '\\ExamInstrBox{}', + snippet: '\\ExamInstrBox{$1}', + meta: 'mathexam-cmd', + score: 0.00035308240943436196 + }, + { + caption: '\\ExamName{}', + snippet: '\\ExamName{$1}', + meta: 'mathexam-cmd', + score: 0.00165391233892938 + }, + { + caption: '\\ExamNameLine', + snippet: '\\ExamNameLine', + meta: 'mathexam-cmd', + score: 0.00165391233892938 + }, + { + caption: '\\ExamClass{}', + snippet: '\\ExamClass{$1}', + meta: 'mathexam-cmd', + score: 0.00165391233892938 + }, + { + caption: '\\ExamHead{}', + snippet: '\\ExamHead{$1}', + meta: 'mathexam-cmd', + score: 0.00165391233892938 + }, + { + caption: '\\answer{}', + snippet: '\\answer{$1}', + meta: 'mathexam-cmd', + score: 0.0034436236729672894 + }, + { + caption: '\\answer', + snippet: '\\answer', + meta: 'mathexam-cmd', + score: 0.0034436236729672894 + }, + { + caption: '\\lhead{}', + snippet: '\\lhead{$1}', + meta: 'mathexam-cmd', + score: 0.05268978171228714 + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'mathexam-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'mathexam-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\fancypagestyle{}{}', + snippet: '\\fancypagestyle{$1}{$2}', + meta: 'mathexam-cmd', + score: 0.009430919590937878 + }, + { + caption: '\\footrule', + snippet: '\\footrule', + meta: 'mathexam-cmd', + score: 0.0010032754348913366 + }, + { + caption: '\\footrule{}', + snippet: '\\footrule{$1}', + meta: 'mathexam-cmd', + score: 0.0010032754348913366 + }, + { + caption: '\\fancyfoot[]{}', + snippet: '\\fancyfoot[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.024973618823189894 + }, + { + caption: '\\fancyfoot{}', + snippet: '\\fancyfoot{$1}', + meta: 'mathexam-cmd', + score: 0.024973618823189894 + }, + { + caption: '\\fancyfootoffset[]{}', + snippet: '\\fancyfootoffset[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.0015373246231684555 + }, + { + caption: '\\fancyfootoffset{}', + snippet: '\\fancyfootoffset{$1}', + meta: 'mathexam-cmd', + score: 0.0015373246231684555 + }, + { + caption: '\\footruleskip', + snippet: '\\footruleskip', + meta: 'mathexam-cmd', + score: 0.000830117957327721 + }, + { + caption: '\\fancyheadoffset[]{}', + snippet: '\\fancyheadoffset[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.0016786568695309166 + }, + { + caption: '\\fancyheadoffset{}', + snippet: '\\fancyheadoffset{$1}', + meta: 'mathexam-cmd', + score: 0.0016786568695309166 + }, + { + caption: '\\iffloatpage{}{}', + snippet: '\\iffloatpage{$1}{$2}', + meta: 'mathexam-cmd', + score: 6.606286310833368e-5 + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'mathexam-cmd', + score: 0.013411641301057813 + }, + { + caption: '\\subsectionmark', + snippet: '\\subsectionmark', + meta: 'mathexam-cmd', + score: 3.1153423008593836e-5 + }, + { + caption: '\\footrulewidth', + snippet: '\\footrulewidth', + meta: 'mathexam-cmd', + score: 0.011424740897486949 + }, + { + caption: '\\fancyhfoffset[]{}', + snippet: '\\fancyhfoffset[$1]{$2}', + meta: 'mathexam-cmd', + score: 3.741978601121172e-5 + }, + { + caption: '\\rhead{}', + snippet: '\\rhead{$1}', + meta: 'mathexam-cmd', + score: 0.022782817416731292 + }, + { + caption: '\\fancyplain{}{}', + snippet: '\\fancyplain{$1}{$2}', + meta: 'mathexam-cmd', + score: 0.007402339896386138 + }, + { + caption: '\\rfoot{}', + snippet: '\\rfoot{$1}', + meta: 'mathexam-cmd', + score: 0.013393817825547868 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mathexam-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\plainheadrulewidth', + snippet: '\\plainheadrulewidth', + meta: 'mathexam-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'mathexam-cmd', + score: 0.03225350148161425 + }, + { + caption: '\\lfoot{}', + snippet: '\\lfoot{$1}', + meta: 'mathexam-cmd', + score: 0.00789399846642229 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'mathexam-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'mathexam-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\fancyhf{}', + snippet: '\\fancyhf{$1}', + meta: 'mathexam-cmd', + score: 0.02314618933449356 + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'mathexam-cmd', + score: 0.005008938879210868 + }, + { + caption: '\\fancyhead[]{}', + snippet: '\\fancyhead[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.039101068064744296 + }, + { + caption: '\\fancyhead{}', + snippet: '\\fancyhead{$1}', + meta: 'mathexam-cmd', + score: 0.039101068064744296 + }, + { + caption: '\\nouppercase{}', + snippet: '\\nouppercase{$1}', + meta: 'mathexam-cmd', + score: 0.006416387071584083 + }, + { + caption: '\\nouppercase', + snippet: '\\nouppercase', + meta: 'mathexam-cmd', + score: 0.006416387071584083 + }, + { + caption: '\\headrule', + snippet: '\\headrule', + meta: 'mathexam-cmd', + score: 0.0008327432627715623 + }, + { + caption: '\\headrule{}', + snippet: '\\headrule{$1}', + meta: 'mathexam-cmd', + score: 0.0008327432627715623 + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'mathexam-cmd', + score: 0.00755042164734884 + }, + { + caption: '\\headrulewidth', + snippet: '\\headrulewidth', + meta: 'mathexam-cmd', + score: 0.02268137935335823 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'mathexam-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'mathexam-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'mathexam-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'mathexam-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'mathexam-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'mathexam-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mathexam-cmd', + score: 0.001042697111754002 + } + ], + floatrow: [ + { + caption: '\\floatfoot{}', + snippet: '\\floatfoot{$1}', + meta: 'floatrow-cmd', + score: 0.0015365464531749851 + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'floatrow-cmd', + score: 0.0008866338267686714 + }, + { + caption: '\\floatsetup[]{}', + snippet: '\\floatsetup[$1]{$2}', + meta: 'floatrow-cmd', + score: 0.0005456136119914056 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.0003890810058478364 + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.0004717618449370015 + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'floatrow-cmd', + score: 5.0133404990680195e-5 + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'floatrow-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'floatrow-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'floatrow-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'floatrow-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'floatrow-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'floatrow-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'floatrow-cmd', + score: 0.00015256647321237863 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'floatrow-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'floatrow-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'floatrow-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'floatrow-cmd', + score: 0.021473212893597875 + } + ], + scrpage2: [ + { + caption: '\\automark[]{}', + snippet: '\\automark[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.0006703031783997437 + }, + { + caption: '\\automark{}', + snippet: '\\automark{$1}', + meta: 'scrpage2-cmd', + score: 0.0006703031783997437 + }, + { + caption: '\\ofoot{}', + snippet: '\\ofoot{$1}', + meta: 'scrpage2-cmd', + score: 0.000605620621498142 + }, + { + caption: '\\ofoot[]{}', + snippet: '\\ofoot[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.000605620621498142 + }, + { + caption: '\\ohead{}', + snippet: '\\ohead{$1}', + meta: 'scrpage2-cmd', + score: 0.004845161937670253 + }, + { + caption: '\\ohead[]{}', + snippet: '\\ohead[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.004845161937670253 + }, + { + caption: '\\headfont', + snippet: '\\headfont', + meta: 'scrpage2-cmd', + score: 0.0011116915941419892 + }, + { + caption: '\\setheadsepline{}', + snippet: '\\setheadsepline{$1}', + meta: 'scrpage2-cmd', + score: 0.00023538827295624133 + }, + { + caption: '\\clearscrheadings', + snippet: '\\clearscrheadings', + meta: 'scrpage2-cmd', + score: 0.0003679125016983611 + }, + { + caption: '\\clearscrheadfoot', + snippet: '\\clearscrheadfoot', + meta: 'scrpage2-cmd', + score: 0.000558377093879783 + }, + { + caption: '\\pagemark', + snippet: '\\pagemark', + meta: 'scrpage2-cmd', + score: 0.0017520841736604843 + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'scrpage2-cmd', + score: 0.00755042164734884 + }, + { + caption: '\\clearscrplain', + snippet: '\\clearscrplain', + meta: 'scrpage2-cmd', + score: 0.00013252422874211978 + }, + { + caption: '\\ifoot{}', + snippet: '\\ifoot{$1}', + meta: 'scrpage2-cmd', + score: 0.0003620142864171218 + }, + { + caption: '\\ifoot[]{}', + snippet: '\\ifoot[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.0003620142864171218 + }, + { + caption: '\\ihead{}', + snippet: '\\ihead{$1}', + meta: 'scrpage2-cmd', + score: 0.0004507603139230655 + }, + { + caption: '\\ihead[]{}', + snippet: '\\ihead[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.0004507603139230655 + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'scrpage2-cmd', + score: 0.013411641301057813 + } + ], + pbox: [ + { + caption: '\\pbox{}{}', + snippet: '\\pbox{$1}{$2}', + meta: 'pbox-cmd', + score: 0.0010883030320478486 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pbox-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pbox-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pbox-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pbox-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pbox-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pbox-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'pbox-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'pbox-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'pbox-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'pbox-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'pbox-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'pbox-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'pbox-cmd', + score: 0.028955796305270766 + } + ], + esint: [ + { + caption: '\\int', + snippet: '\\int', + meta: 'esint-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\iint', + snippet: '\\iint', + meta: 'esint-cmd', + score: 0.003916494384710151 + }, + { + caption: '\\varoiint', + snippet: '\\varoiint', + meta: 'esint-cmd', + score: 0.0001069175284516453 + }, + { + caption: '\\iiint', + snippet: '\\iiint', + meta: 'esint-cmd', + score: 0.0010383179918633135 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'esint-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\oiint', + snippet: '\\oiint', + meta: 'esint-cmd', + score: 7.127835230109687e-5 + } + ], + algorithmicx: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algorithmicx-cmd', + score: 0.0019861803661869416 + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algorithmicx-cmd', + score: 0.008622777195102994 + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algorithmicx-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algorithmicx-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algorithmicx-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algorithmicx-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algorithmicx-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algorithmicx-cmd', + score: 0.005178604573219454 + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algorithmicx-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algorithmicx-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algorithmicx-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algorithmicx-cmd', + score: 0.0007916858220314837 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algorithmicx-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algorithmicx-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algorithmicx-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algorithmicx-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algorithmicx-cmd', + score: 0.0005463612015579842 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithmicx-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithmicx-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithmicx-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithmicx-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithmicx-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithmicx-cmd', + score: 0.0018957469739775527 + } + ], + bibentry: [ + { + caption: '\\bibentry{}', + snippet: '\\bibentry{$1}', + meta: 'bibentry-cmd', + score: 0.002786693424998083 + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'bibentry-cmd', + score: 0.13586474005868793 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'bibentry-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'bibentry-cmd', + score: 3.800886892251021 + }, + { + caption: '\\nobibliography', + snippet: '\\nobibliography', + meta: 'bibentry-cmd', + score: 0.0009870472135074372 + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'bibentry-cmd', + score: 0.2659628337907604 + }, + { + caption: '\\doi{}', + snippet: '\\doi{$1}', + meta: 'bibentry-cmd', + score: 0.004001210811454663 + }, + { + caption: '\\doi', + snippet: '\\doi', + meta: 'bibentry-cmd', + score: 0.004001210811454663 + } + ], + txfonts: [ + { + caption: '\\sqrt{}', + snippet: '\\sqrt{$1}', + meta: 'txfonts-cmd', + score: 0.20240160977404634 + } + ], + ngerman: [ + { + caption: '\\figurename', + snippet: '\\figurename', + meta: 'ngerman-cmd', + score: 0.008169568707145965 + }, + { + caption: '\\figurename{}', + snippet: '\\figurename{$1}', + meta: 'ngerman-cmd', + score: 0.008169568707145965 + }, + { + caption: '\\indexname', + snippet: '\\indexname', + meta: 'ngerman-cmd', + score: 0.0007544109314450072 + }, + { + caption: '\\glqq', + snippet: '\\glqq', + meta: 'ngerman-cmd', + score: 0.0039133256714254504 + }, + { + caption: '\\glqq{}', + snippet: '\\glqq{$1}', + meta: 'ngerman-cmd', + score: 0.0039133256714254504 + }, + { + caption: '\\today', + snippet: '\\today', + meta: 'ngerman-cmd', + score: 0.10733849317324783 + }, + { + caption: '\\bibname', + snippet: '\\bibname', + meta: 'ngerman-cmd', + score: 0.007599529252128519 + }, + { + caption: '\\bibname{}', + snippet: '\\bibname{$1}', + meta: 'ngerman-cmd', + score: 0.007599529252128519 + }, + { + caption: '\\captionsngerman{}', + snippet: '\\captionsngerman{$1}', + meta: 'ngerman-cmd', + score: 0.00010171098214158578 + }, + { + caption: '\\grqq', + snippet: '\\grqq', + meta: 'ngerman-cmd', + score: 0.006659522189248266 + }, + { + caption: '\\grqq{}', + snippet: '\\grqq{$1}', + meta: 'ngerman-cmd', + score: 0.006659522189248266 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'ngerman-cmd', + score: 0.0029238994233674776 + } + ], + eucal: [ + { + caption: '\\mathscr{}', + snippet: '\\mathscr{$1}', + meta: 'eucal-cmd', + score: 0.025302230226027712 + }, + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'eucal-cmd', + score: 0.35084018920966636 + } + ], + ifluatex: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ifluatex-cmd', + score: 0.008565354665444157 + } + ], + chemfig: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemfig-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chemfig-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chemfig-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chemfig-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemfig-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chemfig-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemfig-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chemfig-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chemfig-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemfig-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chemfig-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemfig-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chemfig-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemfig-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chemfig-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemfig-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chemfig-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chemfig-cmd', + score: 0.2864294797053033 + } + ], + abstract: [ + { + caption: '\\abstractnamefont', + snippet: '\\abstractnamefont', + meta: 'abstract-cmd', + score: 6.2350576842596716e-6 + } + ], + 'tikz-cd': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-cd-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-cd-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-cd-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cd-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-cd-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-cd-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-cd-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-cd-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-cd-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cd-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-cd-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-cd-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-cd-cmd', + score: 0.2864294797053033 + } + ], + flowfram: [ + { + caption: '\\framebreak', + snippet: '\\framebreak', + meta: 'flowfram-cmd', + score: 0.004019097827091264 + }, + { + caption: '\\newstaticframe{}{}{}{}', + snippet: '\\newstaticframe{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.0014762683341407986 + }, + { + caption: '\\newflowframe{}{}{}{}[]', + snippet: '\\newflowframe{$1}{$2}{$3}{$4}[$5]', + meta: 'flowfram-cmd', + score: 0.002952536668281597 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flowfram-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'flowfram-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'flowfram-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'flowfram-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'flowfram-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'flowfram-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'flowfram-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'flowfram-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'flowfram-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'flowfram-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'flowfram-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'flowfram-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flowfram-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'flowfram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'flowfram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'flowfram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'flowfram-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'flowfram-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'flowfram-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'flowfram-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'flowfram-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'flowfram-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flowfram-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'flowfram-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'flowfram-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\afterpage{}', + snippet: '\\afterpage{$1}', + meta: 'flowfram-cmd', + score: 0.0018578070791608345 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'flowfram-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'flowfram-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'flowfram-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'flowfram-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'flowfram-cmd', + score: 0.0018957469739775527 + } + ], + marginnote: [ + { + caption: '\\marginnote{}', + snippet: '\\marginnote{$1}', + meta: 'marginnote-cmd', + score: 0.010285502283803235 + }, + { + caption: '\\marginnote', + snippet: '\\marginnote', + meta: 'marginnote-cmd', + score: 0.010285502283803235 + }, + { + caption: '\\raggedleftmarginnote', + snippet: '\\raggedleftmarginnote', + meta: 'marginnote-cmd', + score: 0.0011268470793267921 + } + ], + xfrac: [ + { + caption: '\\sfrac{}{}', + snippet: '\\sfrac{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.0030164694688453453 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'xfrac-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'xfrac-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xfrac-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xfrac-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xfrac-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xfrac-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xfrac-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xfrac-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xfrac-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xfrac-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xfrac-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xfrac-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xfrac-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xfrac-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xfrac-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xfrac-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xfrac-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xfrac-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xfrac-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xfrac-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xfrac-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xfrac-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'xfrac-cmd', + score: 0.0063276692758974925 + } + ], + shortvrb: [ + { + caption: '\\MakeShortVerb{}', + snippet: '\\MakeShortVerb{$1}', + meta: 'shortvrb-cmd', + score: 0.0002890733176655595 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'shortvrb-cmd', + score: 0.009278344180101056 + } + ], + animate: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'animate-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'animate-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'animate-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'animate-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'animate-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'animate-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'animate-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'animate-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'animate-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'animate-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'animate-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'animate-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'animate-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'animate-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'animate-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'animate-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'animate-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'animate-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'animate-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'animate-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'animate-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'animate-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'animate-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'animate-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'animate-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'animate-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'animate-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'animate-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'animate-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'animate-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'animate-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'animate-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'animate-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'animate-cmd', + score: 0.008565354665444157 + } + ], + euscript: [ + { + caption: '\\mathscr{}', + snippet: '\\mathscr{$1}', + meta: 'euscript-cmd', + score: 0.025302230226027712 + }, + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'euscript-cmd', + score: 0.35084018920966636 + } + ], + hhline: [ + { + caption: '\\hhline{}', + snippet: '\\hhline{$1}', + meta: 'hhline-cmd', + score: 0.0004816338278157677 + } + ], + subfiles: [ + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'subfiles-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'subfiles-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\subfile{}', + snippet: '\\subfile{$1}', + meta: 'subfiles-cmd', + score: 0.03337062633525651 + }, + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'subfiles-cmd', + score: 0.0022216421267780076 + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'subfiles-cmd', + score: 0.0072203369120285256 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'subfiles-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'subfiles-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'subfiles-cmd', + score: 0.413853376001159 + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'subfiles-cmd', + score: 0.0024547099784948665 + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'subfiles-cmd', + score: 0.0024547099784948665 + } + ], + accents: [ + { + caption: '\\underaccent{}{}', + snippet: '\\underaccent{$1}{$2}', + meta: 'accents-cmd', + score: 0.00109513727836357 + } + ], + theorem: [ + { + caption: '\\theorembodyfont{}', + snippet: '\\theorembodyfont{$1}', + meta: 'theorem-cmd', + score: 0.00047103366488576113 + } + ], + metalogo: [ + { + caption: '\\XeTeX', + snippet: '\\XeTeX', + meta: 'metalogo-cmd', + score: 0.0010635559050357936 + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'metalogo-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'metalogo-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'metalogo-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'metalogo-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\XeLaTeX', + snippet: '\\XeLaTeX', + meta: 'metalogo-cmd', + score: 0.002009786035379175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'metalogo-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'metalogo-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'metalogo-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'metalogo-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'metalogo-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'metalogo-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'metalogo-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'metalogo-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'metalogo-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'metalogo-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'metalogo-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'metalogo-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'metalogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'metalogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'metalogo-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'metalogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'metalogo-cmd', + score: 0.004719094298848707 + } + ], + bookmark: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'bookmark-cmd', + score: 0.006492248863367502 + }, + { + caption: '\\bookmarkget{}', + snippet: '\\bookmarkget{$1}', + meta: 'bookmark-cmd', + score: 0.00026847053008917257 + }, + { + caption: '\\bookmarksetup{}', + snippet: '\\bookmarksetup{$1}', + meta: 'bookmark-cmd', + score: 0.001134118016265821 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'bookmark-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'bookmark-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'bookmark-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'bookmark-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'bookmark-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'bookmark-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'bookmark-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'bookmark-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'bookmark-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'bookmark-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'bookmark-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'bookmark-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'bookmark-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'bookmark-cmd', + score: 0.009472569279662113 + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'bookmark-cmd', + score: 0.006492248863367502 + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'bookmark-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'bookmark-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'bookmark-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'bookmark-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'bookmark-cmd', + score: 0.017289599800633146 + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'bookmark-cmd', + score: 0.001509072212764015 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bookmark-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'bookmark-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'bookmark-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'bookmark-cmd', + score: 7.849662248028187 + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.00978652043902115 + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'bookmark-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'bookmark-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'bookmark-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'bookmark-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'bookmark-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'bookmark-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'bookmark-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.00029737672328168955 + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.0073781967296121 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'bookmark-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'bookmark-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'bookmark-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'bookmark-cmd', + score: 0.019788865471151957 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'bookmark-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'bookmark-cmd', + score: 3.800886892251021 + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'bookmark-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'bookmark-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'bookmark-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'bookmark-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'bookmark-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'bookmark-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'bookmark-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'bookmark-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.009652820108904094 + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'bookmark-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'bookmark-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'bookmark-cmd', + score: 0.13586474005868793 + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'bookmark-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'bookmark-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.27111130260612365 + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'bookmark-cmd', + score: 0.0038703587462843594 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'bookmark-cmd', + score: 0.03741172773691362 + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'bookmark-cmd', + score: 0.0004995635515943437 + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'bookmark-cmd', + score: 7.847906405228455 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'bookmark-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'bookmark-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'bookmark-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'bookmark-cmd', + score: 6.006262128895586e-5 + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'bookmark-cmd', + score: 0.00017906650306643613 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'bookmark-cmd', + score: 1.4380093454211778 + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'bookmark-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'bookmark-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'bookmark-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'bookmark-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'bookmark-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'bookmark-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'bookmark-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'bookmark-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'bookmark-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'bookmark-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'bookmark-cmd', + score: 0.004515152477030062 + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'bookmark-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'bookmark-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'bookmark-cmd', + score: 0.3311721696201715 + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'bookmark-cmd', + score: 0.06967310843464661 + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'bookmark-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'bookmark-cmd', + score: 0.9202908262245683 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'bookmark-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'bookmark-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'bookmark-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411 + } + ], + anysize: [ + { + caption: '\\marginsize{}{}{}{}', + snippet: '\\marginsize{$1}{$2}{$3}{$4}', + meta: 'anysize-cmd', + score: 0.0012034744434699038 + } + ], + diagbox: [ + { + caption: '\\diagbox[]{}{}', + snippet: '\\diagbox[$1]{$2}{$3}', + meta: 'diagbox-cmd', + score: 2.2176553306779127e-5 + }, + { + caption: '\\backslashbox{}{}', + snippet: '\\backslashbox{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.0005060776550832729 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\Line', + snippet: '\\Line', + meta: 'diagbox-cmd', + score: 0.0006078790177929149 + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'diagbox-cmd', + score: 0.0008987552240147395 + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'diagbox-cmd', + score: 0.014519741542622297 + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'diagbox-cmd', + score: 0.00022468880600368487 + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'diagbox-cmd', + score: 0.002970308722584179 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'diagbox-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'diagbox-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'diagbox-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'diagbox-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'diagbox-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'diagbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'diagbox-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'diagbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'diagbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'diagbox-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'diagbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'diagbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'diagbox-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'diagbox-cmd', + score: 0.028955796305270766 + } + ], + commath: [ + { + caption: '\\dod{}{}', + snippet: '\\dod{$1}{$2}', + meta: 'commath-cmd', + score: 7.950032807135384e-5 + }, + { + caption: '\\dpd{}{}', + snippet: '\\dpd{$1}{$2}', + meta: 'commath-cmd', + score: 0.00022966761442835552 + }, + { + caption: '\\dpd[]{}{}', + snippet: '\\dpd[$1]{$2}{$3}', + meta: 'commath-cmd', + score: 0.00022966761442835552 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'commath-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'commath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'commath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'commath-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'commath-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'commath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'commath-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'commath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'commath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'commath-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'commath-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'commath-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'commath-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'commath-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'commath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'commath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'commath-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'commath-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'commath-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'commath-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'commath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'commath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'commath-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'commath-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'commath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'commath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'commath-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'commath-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'commath-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'commath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'commath-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'commath-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'commath-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'commath-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'commath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'commath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'commath-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'commath-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'commath-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'commath-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'commath-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'commath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'commath-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'commath-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'commath-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'commath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'commath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'commath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'commath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'commath-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'commath-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'commath-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'commath-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'commath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'commath-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'commath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'commath-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'commath-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'commath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'commath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'commath-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'commath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'commath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'commath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'commath-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'commath-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'commath-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'commath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'commath-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'commath-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'commath-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'commath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'commath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'commath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'commath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'commath-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'commath-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'commath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'commath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'commath-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'commath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'commath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'commath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'commath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'commath-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'commath-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'commath-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'commath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'commath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'commath-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'commath-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'commath-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'commath-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'commath-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'commath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'commath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'commath-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'commath-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'commath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'commath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'commath-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'commath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'commath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'commath-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'commath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'commath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'commath-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'commath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'commath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'commath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'commath-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'commath-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'commath-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'commath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'commath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'commath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'commath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'commath-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'commath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'commath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'commath-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'commath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'commath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'commath-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'commath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'commath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'commath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'commath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'commath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'commath-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'commath-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'commath-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'commath-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'commath-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'commath-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'commath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'commath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'commath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'commath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'commath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'commath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'commath-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'commath-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'commath-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'commath-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'commath-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'commath-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'commath-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'commath-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'commath-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'commath-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'commath-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'commath-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'commath-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'commath-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'commath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'commath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'commath-cmd', + score: 0.0063276692758974925 + } + ], + breqn: [ + { + caption: '\\biggl', + snippet: '\\biggl', + meta: 'breqn-cmd', + score: 0.0016066581118686831 + }, + { + caption: '\\biggl[]', + snippet: '\\biggl[$1]', + meta: 'breqn-cmd', + score: 0.0016066581118686831 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'breqn-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'breqn-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'breqn-cmd', + score: 7.847906405228455 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'breqn-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'breqn-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'breqn-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'breqn-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'breqn-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'breqn-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'breqn-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'breqn-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'breqn-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'breqn-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'breqn-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'breqn-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'breqn-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'breqn-cmd', + score: 0.2864294797053033 + } + ], + ClearSans: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ClearSans-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ClearSans-cmd', + score: 0.008565354665444157 + } + ], + ccicons: [ + { + caption: '\\ccbynd', + snippet: '\\ccbynd', + meta: 'ccicons-cmd', + score: 0.0002103469673225986 + }, + { + caption: '\\ccbysa', + snippet: '\\ccbysa', + meta: 'ccicons-cmd', + score: 0.00016986782584471025 + } + ], + varioref: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'varioref-cmd', + score: 0.008565354665444157 + } + ], + SIunits: [ + { + caption: '\\micro', + snippet: '\\micro', + meta: 'SIunits-cmd', + score: 0.011051971930487929 + }, + { + caption: '\\meter', + snippet: '\\meter', + meta: 'SIunits-cmd', + score: 0.012499244923238213 + }, + { + caption: '\\cdot', + snippet: '\\cdot', + meta: 'SIunits-cmd', + score: 0.23029085545522762 + }, + { + caption: '\\degreecelsius', + snippet: '\\degreecelsius', + meta: 'SIunits-cmd', + score: 0.002130669712103909 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'SIunits-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'SIunits-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'SIunits-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'SIunits-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'SIunits-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'SIunits-cmd', + score: 0.0063276692758974925 + } + ], + alltt: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'alltt-cmd', + score: 0.413853376001159 + } + ], + fancyvrb: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fancyvrb-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fancyvrb-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'fancyvrb-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'fancyvrb-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fancyvrb-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\fvset{}', + snippet: '\\fvset{$1}', + meta: 'fancyvrb-cmd', + score: 0.00015476887282479622 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'fancyvrb-cmd', + score: 0.00037306820619479756 + } + ], + textgreek: [ + { + caption: '\\temp', + snippet: '\\temp', + meta: 'textgreek-cmd', + score: 0.0003566413345844499 + }, + { + caption: '\\temp{}', + snippet: '\\temp{$1}', + meta: 'textgreek-cmd', + score: 0.0003566413345844499 + } + ], + endnotes: [ + { + caption: '\\endnote', + snippet: '\\endnote', + meta: 'endnotes-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\theendnotes', + snippet: '\\theendnotes', + meta: 'endnotes-cmd', + score: 0.0002788252334941383 + } + ], + leading: [ + { + caption: '\\leading{}', + snippet: '\\leading{$1}', + meta: 'leading-cmd', + score: 0.00029077374894594517 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'leading-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'leading-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'leading-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'leading-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'leading-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'leading-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'leading-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'leading-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'leading-cmd', + score: 0.028955796305270766 + } + ], + esvect: [ + { + caption: '\\vv', + snippet: '\\vv', + meta: 'esvect-cmd', + score: 0.003087420708479709 + }, + { + caption: '\\vv{}', + snippet: '\\vv{$1}', + meta: 'esvect-cmd', + score: 0.003087420708479709 + } + ], + lettrine: [ + { + caption: '\\LettrineFontHook', + snippet: '\\LettrineFontHook', + meta: 'lettrine-cmd', + score: 9.103413871235853e-5 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'lettrine-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'lettrine-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'lettrine-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\lettrine[]{}{}', + snippet: '\\lettrine[$1]{$2}{$3}', + meta: 'lettrine-cmd', + score: 0.0028028146688245602 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'lettrine-cmd', + score: 0.00037306820619479756 + } + ], + pgfopts: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfopts-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfopts-cmd', + score: 0.021170869458413965 + } + ], + tabulary: [ + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabulary-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabulary-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tabulary-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tabulary-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'tabulary-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabulary-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'tabulary-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabulary-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'tabulary-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tabulary-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'tabulary-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'tabulary-cmd', + score: 0.018615449342361392 + } + ], + grffile: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'grffile-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'grffile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'grffile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'grffile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'grffile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'grffile-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'grffile-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'grffile-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'grffile-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'grffile-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'grffile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'grffile-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'grffile-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'grffile-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'grffile-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'grffile-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'grffile-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'grffile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'grffile-cmd', + score: 0.021170869458413965 + } + ], + pgfgantt: [ + { + caption: '\\gantttitlecalendar{}', + snippet: '\\gantttitlecalendar{$1}', + meta: 'pgfgantt-cmd', + score: 0.00027821409061195467 + }, + { + caption: '\\ganttset{}', + snippet: '\\ganttset{$1}', + meta: 'pgfgantt-cmd', + score: 0.0002492292297037303 + }, + { + caption: '\\gantttitlelist[]{}{}', + snippet: '\\gantttitlelist[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.00046430963549633653 + }, + { + caption: '\\gantttitlelist{}{}', + snippet: '\\gantttitlelist{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.00046430963549633653 + }, + { + caption: '\\ganttlink[]{}{}', + snippet: '\\ganttlink[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.0011494045501518014 + }, + { + caption: '\\newganttchartelement{}{}', + snippet: '\\newganttchartelement{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.00023651453263545777 + }, + { + caption: '\\gantttitle{}{}', + snippet: '\\gantttitle{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.001804531670553746 + }, + { + caption: '\\gantttitle[]{}{}', + snippet: '\\gantttitle[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.001804531670553746 + }, + { + caption: '\\setganttlinklabel{}{}', + snippet: '\\setganttlinklabel{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 9.045112044064169e-5 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfgantt-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfgantt-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfgantt-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfgantt-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfgantt-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfgantt-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfgantt-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfgantt-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfgantt-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfgantt-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfgantt-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfgantt-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfgantt-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfgantt-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfgantt-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfgantt-cmd', + score: 0.2864294797053033 + } + ], + circuitikz: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'circuitikz-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'circuitikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'circuitikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'circuitikz-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'circuitikz-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'circuitikz-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'circuitikz-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'circuitikz-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'circuitikz-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'circuitikz-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'circuitikz-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'circuitikz-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'circuitikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'circuitikz-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'circuitikz-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'circuitikz-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'circuitikz-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'circuitikz-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'circuitikz-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'circuitikz-cmd', + score: 0.2864294797053033 + } + ], + hypcap: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypcap-cmd', + score: 0.008565354665444157 + } + ], + 'scrlayer-scrpage': [ + { + caption: '\\lofoot{}', + snippet: '\\lofoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00011911213812243537 + }, + { + caption: '\\rofoot{}', + snippet: '\\rofoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00021082185485863327 + }, + { + caption: '\\clearpairofpagestyles', + snippet: '\\clearpairofpagestyles', + meta: 'scrlayer-scrpage-cmd', + score: 8.874602750594376e-5 + }, + { + caption: '\\ihead{}', + snippet: '\\ihead{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0004507603139230655 + }, + { + caption: '\\ihead[]{}', + snippet: '\\ihead[$1]{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0004507603139230655 + }, + { + caption: '\\cofoot{}', + snippet: '\\cofoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00021082185485863327 + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.013411641301057813 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrlayer-scrpage-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrlayer-scrpage-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0008555564394100388 + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.012985816912639263 + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.000396664302361659 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\automark[]{}', + snippet: '\\automark[$1]{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0006703031783997437 + }, + { + caption: '\\automark{}', + snippet: '\\automark{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0006703031783997437 + }, + { + caption: '\\pagemark', + snippet: '\\pagemark', + meta: 'scrlayer-scrpage-cmd', + score: 0.0017520841736604843 + } + ], + amsgen: [ + { + caption: '\\do', + snippet: '\\do', + meta: 'amsgen-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsgen-cmd', + score: 0.0063276692758974925 + } + ], + tipa: [ + { + caption: '\\textipa{}', + snippet: '\\textipa{$1}', + meta: 'tipa-cmd', + score: 0.0028202799587687334 + } + ], + appendixnumberbeamer: [ + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'appendixnumberbeamer-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\inserttotalframenumber', + snippet: '\\inserttotalframenumber', + meta: 'appendixnumberbeamer-cmd', + score: 0.0008756113669543194 + } + ], + totcount: [ + { + caption: '\\totvalue{}', + snippet: '\\totvalue{$1}', + meta: 'totcount-cmd', + score: 0.000325977535138643 + }, + { + caption: '\\newtotcounter{}', + snippet: '\\newtotcounter{$1}', + meta: 'totcount-cmd', + score: 0.004398151085448998 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'totcount-cmd', + score: 0.00037306820619479756 + } + ], + atbegshi: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'atbegshi-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'atbegshi-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'atbegshi-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'atbegshi-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'atbegshi-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'atbegshi-cmd', + score: 0.008565354665444157 + } + ], + environ: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'environ-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'environ-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'environ-cmd', + score: 0.021170869458413965 + } + ], + arydshln: [ + { + caption: '\\hdashline', + snippet: '\\hdashline', + meta: 'arydshln-cmd', + score: 3.1727559255976046e-5 + }, + { + caption: '\\arrayrulecolor{}', + snippet: '\\arrayrulecolor{$1}', + meta: 'arydshln-cmd', + score: 0.008538501902241319 + }, + { + caption: '\\arrayrulecolor[]{}', + snippet: '\\arrayrulecolor[$1]{$2}', + meta: 'arydshln-cmd', + score: 0.008538501902241319 + }, + { + caption: '\\hline', + snippet: '\\hline', + meta: 'arydshln-cmd', + score: 1.3209538327406387 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'arydshln-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\cline{}', + snippet: '\\cline{$1}', + meta: 'arydshln-cmd', + score: 0.07276573550543858 + } + ], + fp: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fp-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fp-cmd', + score: 0.021170869458413965 + } + ], + here: [ + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'here-cmd', + score: 0.0009837365348002915 + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'here-cmd', + score: 0.0005815474978918903 + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'here-cmd', + score: 0.0008866338267686714 + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'here-cmd', + score: 0.0015470917047414941 + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'here-cmd', + score: 0.0011934321931750752 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'here-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'here-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'here-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'here-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'here-cmd', + score: 0.0012745874472536625 + } + ], + layout: [ + { + caption: '\\layout', + snippet: '\\layout', + meta: 'layout-cmd', + score: 0.0003951770756385293 + }, + { + caption: '\\layout{}', + snippet: '\\layout{$1}', + meta: 'layout-cmd', + score: 0.0003951770756385293 + } + ], + multibib: [ + { + caption: '\\newcites{}{}', + snippet: '\\newcites{$1}{$2}', + meta: 'multibib-cmd', + score: 0.0024438508435048224 + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'multibib-cmd', + score: 0.2659628337907604 + } + ], + tgpagella: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgpagella-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgpagella-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgpagella-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgpagella-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgpagella-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgpagella-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgpagella-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgpagella-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgpagella-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgpagella-cmd', + score: 0.021170869458413965 + } + ], + minitoc: [ + { + caption: '\\addstarredchapter{}', + snippet: '\\addstarredchapter{$1}', + meta: 'minitoc-cmd', + score: 0.0009796486230293261 + }, + { + caption: '\\minitoc', + snippet: '\\minitoc', + meta: 'minitoc-cmd', + score: 0.001626371504530358 + }, + { + caption: '\\dominitoc', + snippet: '\\dominitoc', + meta: 'minitoc-cmd', + score: 0.0006984399207241325 + }, + { + caption: '\\mtcaddchapter', + snippet: '\\mtcaddchapter', + meta: 'minitoc-cmd', + score: 9.045112044064169e-5 + }, + { + caption: '\\listoffigures', + snippet: '\\listoffigures', + meta: 'minitoc-cmd', + score: 0.03447318897846567 + }, + { + caption: '\\listoftables', + snippet: '\\listoftables', + meta: 'minitoc-cmd', + score: 0.02104656820469027 + }, + { + caption: '\\tableofcontents', + snippet: '\\tableofcontents', + meta: 'minitoc-cmd', + score: 0.13360595130994957 + }, + { + caption: '\\adjustmtc', + snippet: '\\adjustmtc', + meta: 'minitoc-cmd', + score: 0.00015075186740106945 + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'minitoc-cmd', + score: 3.0952612541683835 + } + ], + nameref: [ + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'nameref-cmd', + score: 0.009472569279662113 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'nameref-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'nameref-cmd', + score: 1.4380093454211778 + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'nameref-cmd', + score: 0.019788865471151957 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'nameref-cmd', + score: 1.897791904799601 + }, + { + caption: '\\thepage', + snippet: '\\thepage', + meta: 'nameref-cmd', + score: 0.0591555998103519 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nameref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nameref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nameref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nameref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'nameref-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nameref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nameref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nameref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nameref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nameref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nameref-cmd', + score: 0.008565354665444157 + } + ], + ntheorem: [ + { + caption: '\\theoremclass{}', + snippet: '\\theoremclass{$1}', + meta: 'ntheorem-cmd', + score: 0.0001448542182198375 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'ntheorem-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'ntheorem-cmd', + score: 0.02533412165007986 + }, + { + caption: '\\newshadedtheorem{}{}', + snippet: '\\newshadedtheorem{$1}{$2}', + meta: 'ntheorem-cmd', + score: 0.0001632850673327423 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'ntheorem-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'ntheorem-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'ntheorem-cmd', + score: 0.215689795055434 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'ntheorem-cmd', + score: 1.897791904799601 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'ntheorem-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'ntheorem-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'ntheorem-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'ntheorem-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'ntheorem-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'ntheorem-cmd', + score: 0.0018957469739775527 + } + ], + tabto: [ + { + caption: '\\tab', + snippet: '\\tab', + meta: 'tabto-cmd', + score: 0.016398493343291305 + }, + { + caption: '\\tab{}', + snippet: '\\tab{$1}', + meta: 'tabto-cmd', + score: 0.016398493343291305 + }, + { + caption: '\\NumTabs{}', + snippet: '\\NumTabs{$1}', + meta: 'tabto-cmd', + score: 0.00011350525217178113 + }, + { + caption: '\\tabto{}{}', + snippet: '\\tabto{$1}{$2}', + meta: 'tabto-cmd', + score: 0.002119919034744357 + }, + { + caption: '\\tabto{}', + snippet: '\\tabto{$1}', + meta: 'tabto-cmd', + score: 0.002119919034744357 + } + ], + emptypage: [ + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'emptypage-cmd', + score: 0.044016804142963585 + } + ], + abntex2abrev: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'abntex2abrev-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'abntex2abrev-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'abntex2abrev-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'abntex2abrev-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'abntex2abrev-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'abntex2abrev-cmd', + score: 0.0018957469739775527 + } + ], + scrhack: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrhack-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrhack-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.0008555564394100388 + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.012985816912639263 + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrhack-cmd', + score: 0.000396664302361659 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\xpatchcmd{}{}{}{}{}', + snippet: '\\xpatchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'scrhack-cmd', + score: 0.0019344877752147675 + }, + { + caption: '\\xpatchcmd', + snippet: '\\xpatchcmd', + meta: 'scrhack-cmd', + score: 0.0019344877752147675 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'scrhack-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'scrhack-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'scrhack-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'scrhack-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'scrhack-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'scrhack-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'scrhack-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'scrhack-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'scrhack-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'scrhack-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'scrhack-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scrhack-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'scrhack-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'scrhack-cmd', + score: 0.2864294797053033 + } + ], + nth: [ + { + caption: '\\nth{}', + snippet: '\\nth{$1}', + meta: 'nth-cmd', + score: 0.0006155314043974968 + }, + { + caption: '\\thesection', + snippet: '\\thesection', + meta: 'nth-cmd', + score: 0.011068945893347528 + }, + { + caption: '\\thesection{}', + snippet: '\\thesection{$1}', + meta: 'nth-cmd', + score: 0.011068945893347528 + } + ], + showkeys: [ + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'showkeys-cmd', + score: 1.897791904799601 + } + ], + fncychap: [ + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'fncychap-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\ChTitleVar{}', + snippet: '\\ChTitleVar{$1}', + meta: 'fncychap-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\thechapter', + snippet: '\\thechapter', + meta: 'fncychap-cmd', + score: 0.011821300392639589 + } + ], + ae: [ + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'ae-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'ae-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ae-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ae-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ae-cmd', + score: 0.021170869458413965 + } + ], + asymptote: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'asymptote-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'asymptote-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'asymptote-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'asymptote-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'asymptote-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'asymptote-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'asymptote-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'asymptote-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'asymptote-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'asymptote-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'asymptote-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'asymptote-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'asymptote-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'asymptote-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'asymptote-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'asymptote-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'asymptote-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'asymptote-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'asymptote-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'asymptote-cmd', + score: 0.008565354665444157 + } + ], + truncate: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'truncate-cmd', + score: 0.04598628699063736 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'truncate-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'truncate-cmd', + score: 0.021170869458413965 + } + ], + xpatch: [ + { + caption: '\\xpatchcmd{}{}{}{}{}', + snippet: '\\xpatchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xpatch-cmd', + score: 0.0019344877752147675 + }, + { + caption: '\\xpatchcmd', + snippet: '\\xpatchcmd', + meta: 'xpatch-cmd', + score: 0.0019344877752147675 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'xpatch-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'xpatch-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'xpatch-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'xpatch-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xpatch-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'xpatch-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xpatch-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'xpatch-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'xpatch-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'xpatch-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xpatch-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'xpatch-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'xpatch-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xpatch-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xpatch-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xpatch-cmd', + score: 0.2864294797053033 + } + ], + totpages: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'totpages-cmd', + score: 0.00037306820619479756 + } + ], + fourier: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fourier-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fourier-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fourier-cmd', + score: 0.021170869458413965 + } + ], + scrbase: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrbase-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrbase-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrbase-cmd', + score: 0.00037306820619479756 + } + ], + svg: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'svg-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'svg-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'svg-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'svg-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'svg-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'svg-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'svg-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'svg-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'svg-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'svg-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'svg-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'svg-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'svg-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'svg-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'svg-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'svg-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'svg-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'svg-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'svg-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'svg-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'svg-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'svg-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'svg-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'svg-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157 + } + ], + etex: [ + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'etex-cmd', + score: 0.0018653410309739879 + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'etex-cmd', + score: 0.00031058155311734754 + } + ], + linguex: [ + { + caption: '\\Last[]', + snippet: '\\Last[$1]', + meta: 'linguex-cmd', + score: 0.0008163755131430334 + }, + { + caption: '\\Last', + snippet: '\\Last', + meta: 'linguex-cmd', + score: 0.0008163755131430334 + }, + { + caption: '\\Next', + snippet: '\\Next', + meta: 'linguex-cmd', + score: 0.0018776636802289772 + }, + { + caption: '\\Next[]', + snippet: '\\Next[$1]', + meta: 'linguex-cmd', + score: 0.0018776636802289772 + }, + { + caption: '\\LLast[]', + snippet: '\\LLast[$1]', + meta: 'linguex-cmd', + score: 0.00016327510262860667 + }, + { + caption: '\\LLast', + snippet: '\\LLast', + meta: 'linguex-cmd', + score: 0.00016327510262860667 + }, + { + caption: '\\NNext[]', + snippet: '\\NNext[$1]', + meta: 'linguex-cmd', + score: 0.0004490065322286684 + }, + { + caption: '\\NNext', + snippet: '\\NNext', + meta: 'linguex-cmd', + score: 0.0004490065322286684 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'linguex-cmd', + score: 1.897791904799601 + }, + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'linguex-cmd', + score: 0.07560370351316588 + } + ], + adforn: [ + { + caption: '\\adforn{}', + snippet: '\\adforn{$1}', + meta: 'adforn-cmd', + score: 0.0003148505561835075 + }, + { + caption: '\\ding{}', + snippet: '\\ding{$1}', + meta: 'adforn-cmd', + score: 0.009992300665793867 + } + ], + bigstrut: [ + { + caption: '\\bigstrut', + snippet: '\\bigstrut', + meta: 'bigstrut-cmd', + score: 0.005498219710082848 + } + ], + standalone: [ + { + caption: '\\renewcommand{}{}', + snippet: '\\renewcommand{$1}{$2}', + meta: 'standalone-cmd', + score: 0.3267437011085663 + }, + { + caption: '\\renewcommand', + snippet: '\\renewcommand', + meta: 'standalone-cmd', + score: 0.3267437011085663 + }, + { + caption: '\\currfiledir', + snippet: '\\currfiledir', + meta: 'standalone-cmd', + score: 0.0002459788020229296 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'standalone-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'standalone-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'standalone-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'standalone-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'standalone-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'standalone-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'standalone-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'standalone-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'standalone-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'standalone-cmd', + score: 0.021170869458413965 + } + ], + ifsym: [ + { + caption: '\\Letter', + snippet: '\\Letter', + meta: 'ifsym-cmd', + score: 0.0012281130571092198 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'ifsym-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ifsym-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ifsym-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'ifsym-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'ifsym-cmd', + score: 0.028955796305270766 + } + ], + newtxtext: [ + { + caption: '\\textsc{}', + snippet: '\\textsc{$1}', + meta: 'newtxtext-cmd', + score: 0.6926466355384758 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'newtxtext-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'newtxtext-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'newtxtext-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'newtxtext-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'newtxtext-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'newtxtext-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'newtxtext-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'newtxtext-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'newtxtext-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'newtxtext-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'newtxtext-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'newtxtext-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'newtxtext-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'newtxtext-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'newtxtext-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'newtxtext-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxtext-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxtext-cmd', + score: 0.008565354665444157 + } + ], + silence: [ + { + caption: '\\WarningsOff[]', + snippet: '\\WarningsOff[$1]', + meta: 'silence-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\WarningFilter{}{}', + snippet: '\\WarningFilter{$1}{$2}', + meta: 'silence-cmd', + score: 0.0010293824370507024 + } + ], + numprint: [ + { + caption: '\\textcelsius', + snippet: '\\textcelsius', + meta: 'numprint-cmd', + score: 0.00012244782670334462 + }, + { + caption: '\\pm', + snippet: '\\pm', + meta: 'numprint-cmd', + score: 0.15663535405975132 + }, + { + caption: '\\npdecimalsign{}', + snippet: '\\npdecimalsign{$1}', + meta: 'numprint-cmd', + score: 8.401009062000455e-6 + }, + { + caption: '\\npthousandsep{}', + snippet: '\\npthousandsep{$1}', + meta: 'numprint-cmd', + score: 8.401009062000455e-6 + }, + { + caption: '\\np{}', + snippet: '\\np{$1}', + meta: 'numprint-cmd', + score: 0.0001782233963311367 + }, + { + caption: '\\np', + snippet: '\\np', + meta: 'numprint-cmd', + score: 0.0001782233963311367 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'numprint-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'numprint-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'numprint-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'numprint-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'numprint-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'numprint-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'numprint-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'numprint-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'numprint-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'numprint-cmd', + score: 0.018615449342361392 + } + ], + srcltx: [ + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'srcltx-cmd', + score: 0.2659628337907604 + }, + { + caption: '\\input{}', + snippet: '\\input{$1}', + meta: 'srcltx-cmd', + score: 0.4966021927742672 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'srcltx-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'srcltx-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'srcltx-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'srcltx-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'srcltx-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'srcltx-cmd', + score: 0.0018957469739775527 + } + ], + ctable: [ + { + caption: '\\tmark[]', + snippet: '\\tmark[$1]', + meta: 'ctable-cmd', + score: 0.004423748442334348 + }, + { + caption: '\\ctable[]{}{}{}', + snippet: '\\ctable[$1]{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.0007377841391165772 + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'ctable-cmd', + score: 0.03789745970461662 + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ctable-cmd', + score: 0.0008038857295393196 + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ctable-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ctable-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ctable-cmd', + score: 0.0005861357565780464 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ctable-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'ctable-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ctable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ctable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'ctable-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'ctable-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'ctable-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ctable-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'ctable-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'ctable-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'ctable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'ctable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'ctable-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'ctable-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'ctable-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'ctable-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'ctable-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.004974385202605165 + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'ctable-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'ctable-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'ctable-cmd', + score: 0.04533364657852219 + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'ctable-cmd', + score: 0.07098077735912875 + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'ctable-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'ctable-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'ctable-cmd', + score: 0.059857788139528495 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'ctable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'ctable-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ctable-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ctable-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ctable-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ctable-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ctable-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ctable-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'ctable-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'ctable-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'ctable-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'ctable-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'ctable-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'ctable-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'ctable-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'ctable-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'ctable-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'ctable-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ctable-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'ctable-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'ctable-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'ctable-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ctable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ctable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'ctable-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'ctable-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'ctable-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ctable-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'ctable-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'ctable-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'ctable-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'ctable-cmd', + score: 0.2864294797053033 + } + ], + bbding: [ + { + caption: '\\HandRight', + snippet: '\\HandRight', + meta: 'bbding-cmd', + score: 9.986169155719329e-5 + }, + { + caption: '\\XSolidBrush', + snippet: '\\XSolidBrush', + meta: 'bbding-cmd', + score: 0.0003502234425563509 + }, + { + caption: '\\Checkmark', + snippet: '\\Checkmark', + meta: 'bbding-cmd', + score: 0.0010506703276690528 + } + ], + endfloat: [ + { + caption: '\\DeclareDelayedFloatFlavor{}{}', + snippet: '\\DeclareDelayedFloatFlavor{$1}{$2}', + meta: 'endfloat-cmd', + score: 0.00012872796177294446 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'endfloat-cmd', + score: 0.00037306820619479756 + } + ], + centernot: [ + { + caption: '\\centernot', + snippet: '\\centernot', + meta: 'centernot-cmd', + score: 0.0002513707969474898 + } + ], + tikzpagenodes: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikzpagenodes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikzpagenodes-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpagenodes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikzpagenodes-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpagenodes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\checkoddpage', + snippet: '\\checkoddpage', + meta: 'tikzpagenodes-cmd', + score: 0.00028672585452906425 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpagenodes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.2864294797053033 + } + ], + xargs: [ + { + caption: '\\newcommandx{}[][]{}', + snippet: '\\newcommandx{$1}[$2][$3]{$4}', + meta: 'xargs-cmd', + score: 0.0001110821063389004 + } + ], + morefloats: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'morefloats-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'morefloats-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'morefloats-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'morefloats-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'morefloats-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'morefloats-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'morefloats-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'morefloats-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'morefloats-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'morefloats-cmd', + score: 0.021170869458413965 + } + ], + background: [ + { + caption: '\\BgThispage', + snippet: '\\BgThispage', + meta: 'background-cmd', + score: 0.0003956357273698423 + }, + { + caption: '\\backgroundsetup{}', + snippet: '\\backgroundsetup{$1}', + meta: 'background-cmd', + score: 0.0004910777123492879 + } + ], + bibunits: [ + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'bibunits-cmd', + score: 0.2659628337907604 + } + ], + moresize: [ + { + caption: '\\Huge', + snippet: '\\Huge', + meta: 'moresize-cmd', + score: 0.04725806985998919 + } + ], + pgfpages: [ + { + caption: '\\pgfpagesphysicalpageoptions{}', + snippet: '\\pgfpagesphysicalpageoptions{$1}', + meta: 'pgfpages-cmd', + score: 0.00045967325420052095 + }, + { + caption: '\\pgfpageslogicalpageoptions{}{}', + snippet: '\\pgfpageslogicalpageoptions{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.00045967325420052095 + }, + { + caption: '\\pgfpageoptionborder{}', + snippet: '\\pgfpageoptionborder{$1}', + meta: 'pgfpages-cmd', + score: 0.0009193465084010419 + }, + { + caption: '\\pgfpageoptionborder', + snippet: '\\pgfpageoptionborder', + meta: 'pgfpages-cmd', + score: 0.0009193465084010419 + }, + { + caption: '\\pgfpagesdeclarelayout{}{}{}', + snippet: '\\pgfpagesdeclarelayout{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.00045967325420052095 + }, + { + caption: '\\pgfpagesuselayout{}', + snippet: '\\pgfpagesuselayout{$1}', + meta: 'pgfpages-cmd', + score: 0.0006090132461062934 + }, + { + caption: '\\pgfpagesuselayout{}[]', + snippet: '\\pgfpagesuselayout{$1}[$2]', + meta: 'pgfpages-cmd', + score: 0.0006090132461062934 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'pgfpages-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'pgfpages-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'pgfpages-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfpages-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfpages-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfpages-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfpages-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfpages-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfpages-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfpages-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfpages-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfpages-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfpages-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfpages-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfpages-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfpages-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfpages-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfpages-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfpages-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfpages-cmd', + score: 0.2864294797053033 + } + ], + ctex: [ + { + caption: '\\CTeX', + snippet: '\\CTeX', + meta: 'ctex-cmd', + score: 0.0005884706823906032 + }, + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'ctex-cmd', + score: 0.04598628699063736 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'ctex-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'ctex-cmd', + score: 0.2864294797053033 + } + ], + algcompatible: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algcompatible-cmd', + score: 0.0019861803661869416 + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algcompatible-cmd', + score: 0.008622777195102994 + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algcompatible-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algcompatible-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algcompatible-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algcompatible-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algcompatible-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algcompatible-cmd', + score: 0.005178604573219454 + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algcompatible-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algcompatible-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algcompatible-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algcompatible-cmd', + score: 0.0007916858220314837 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algcompatible-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algcompatible-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algcompatible-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algcompatible-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algcompatible-cmd', + score: 0.0005463612015579842 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algcompatible-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algcompatible-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algcompatible-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algcompatible-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algcompatible-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algcompatible-cmd', + score: 0.0018957469739775527 + } + ], + draftwatermark: [ + { + caption: '\\SetWatermarkScale{}', + snippet: '\\SetWatermarkScale{$1}', + meta: 'draftwatermark-cmd', + score: 0.0013776850432469145 + }, + { + caption: '\\SetWatermarkText{}', + snippet: '\\SetWatermarkText{$1}', + meta: 'draftwatermark-cmd', + score: 0.0017209596079747669 + }, + { + caption: '\\SetWatermarkColor[]{}', + snippet: '\\SetWatermarkColor[$1]{$2}', + meta: 'draftwatermark-cmd', + score: 0.0007061648188687239 + }, + { + caption: '\\SetWatermarkFontSize{}', + snippet: '\\SetWatermarkFontSize{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005747853176838451 + }, + { + caption: '\\SetWatermarkLightness{}', + snippet: '\\SetWatermarkLightness{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005747853176838451 + }, + { + caption: '\\SetWatermarkAngle{}', + snippet: '\\SetWatermarkAngle{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005747853176838451 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'draftwatermark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'draftwatermark-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'draftwatermark-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'draftwatermark-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'draftwatermark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'draftwatermark-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'draftwatermark-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'draftwatermark-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'draftwatermark-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'draftwatermark-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'draftwatermark-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'draftwatermark-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'draftwatermark-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707 + } + ], + eqparbox: [ + { + caption: '\\eqparbox{}{}', + snippet: '\\eqparbox{$1}{$2}', + meta: 'eqparbox-cmd', + score: 2.9423534119530166e-5 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'eqparbox-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'eqparbox-cmd', + score: 3.800886892251021 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'eqparbox-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'eqparbox-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'eqparbox-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'eqparbox-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'eqparbox-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqparbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'eqparbox-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'eqparbox-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqparbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'eqparbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'eqparbox-cmd', + score: 0.021170869458413965 + } + ], + nowidow: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nowidow-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nowidow-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nowidow-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nowidow-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nowidow-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nowidow-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nowidow-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nowidow-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nowidow-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nowidow-cmd', + score: 0.021170869458413965 + } + ], + stackrel: [ + { + caption: '\\stackrel{}{}', + snippet: '\\stackrel{$1}{$2}', + meta: 'stackrel-cmd', + score: 0.009911875742973681 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'stackrel-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'stackrel-cmd', + score: 0.002958865219480927 + } + ], + threeparttablex: [ + { + caption: '\\item', + snippet: '\\item', + meta: 'threeparttablex-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'threeparttablex-cmd', + score: 3.800886892251021 + }, + { + caption: '\\insertTableNotes', + snippet: '\\insertTableNotes', + meta: 'threeparttablex-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\tnotex{}', + snippet: '\\tnotex{$1}', + meta: 'threeparttablex-cmd', + score: 0.0021491972748178554 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'threeparttablex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'threeparttablex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'threeparttablex-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'threeparttablex-cmd', + score: 3.800886892251021 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'threeparttablex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'threeparttablex-cmd', + score: 0.021170869458413965 + } + ], + mathdesign: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mathdesign-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mathdesign-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mathdesign-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'mathdesign-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'mathdesign-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'mathdesign-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'mathdesign-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'mathdesign-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'mathdesign-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'mathdesign-cmd', + score: 0.00037306820619479756 + } + ], + 'pst-node': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-node-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-node-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-node-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-node-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-node-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-node-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-node-cmd', + score: 0.006520475264573554 + } + ], + varwidth: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'varwidth-cmd', + score: 0.413853376001159 + } + ], + schemabloc: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'schemabloc-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'schemabloc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'schemabloc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'schemabloc-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'schemabloc-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'schemabloc-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'schemabloc-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'schemabloc-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'schemabloc-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'schemabloc-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'schemabloc-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'schemabloc-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'schemabloc-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'schemabloc-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'schemabloc-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'schemabloc-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'schemabloc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'schemabloc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'schemabloc-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'schemabloc-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'schemabloc-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'schemabloc-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'schemabloc-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'schemabloc-cmd', + score: 0.2864294797053033 + } + ], + bigints: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'bigints-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'bigints-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'bigints-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'bigints-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'bigints-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bigints-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'bigints-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'bigints-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'bigints-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'bigints-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'bigints-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'bigints-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'bigints-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'bigints-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'bigints-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'bigints-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'bigints-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'bigints-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'bigints-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'bigints-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'bigints-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'bigints-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'bigints-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'bigints-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'bigints-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'bigints-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'bigints-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'bigints-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'bigints-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'bigints-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'bigints-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'bigints-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'bigints-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'bigints-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'bigints-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'bigints-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'bigints-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'bigints-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'bigints-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'bigints-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'bigints-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'bigints-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'bigints-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'bigints-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'bigints-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'bigints-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'bigints-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'bigints-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'bigints-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'bigints-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'bigints-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'bigints-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'bigints-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'bigints-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'bigints-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'bigints-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'bigints-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'bigints-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'bigints-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'bigints-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'bigints-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'bigints-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'bigints-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'bigints-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'bigints-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'bigints-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'bigints-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'bigints-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'bigints-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'bigints-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'bigints-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'bigints-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'bigints-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'bigints-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'bigints-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'bigints-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'bigints-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'bigints-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'bigints-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'bigints-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'bigints-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'bigints-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'bigints-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'bigints-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'bigints-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'bigints-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'bigints-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'bigints-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'bigints-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'bigints-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'bigints-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'bigints-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'bigints-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'bigints-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'bigints-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'bigints-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'bigints-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'bigints-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'bigints-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'bigints-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'bigints-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'bigints-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'bigints-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'bigints-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'bigints-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'bigints-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'bigints-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'bigints-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'bigints-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'bigints-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'bigints-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'bigints-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'bigints-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'bigints-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'bigints-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'bigints-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'bigints-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'bigints-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'bigints-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'bigints-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'bigints-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'bigints-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'bigints-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'bigints-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'bigints-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'bigints-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'bigints-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'bigints-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'bigints-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'bigints-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'bigints-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'bigints-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'bigints-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'bigints-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'bigints-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'bigints-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'bigints-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'bigints-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'bigints-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'bigints-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'bigints-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'bigints-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'bigints-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'bigints-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'bigints-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'bigints-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'bigints-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'bigints-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bigints-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bigints-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'bigints-cmd', + score: 0.0063276692758974925 + } + ], + classicthesis: [ + { + caption: '\\marginpar{}', + snippet: '\\marginpar{$1}', + meta: 'classicthesis-cmd', + score: 0.003400158497921723 + }, + { + caption: '\\marginpar', + snippet: '\\marginpar', + meta: 'classicthesis-cmd', + score: 0.003400158497921723 + }, + { + caption: '\\cftsecleader', + snippet: '\\cftsecleader', + meta: 'classicthesis-cmd', + score: 0.0011340882025681251 + }, + { + caption: '\\cftsubsecleader', + snippet: '\\cftsubsecleader', + meta: 'classicthesis-cmd', + score: 1.0644172549700836e-5 + }, + { + caption: '\\spacedlowsmallcaps{}', + snippet: '\\spacedlowsmallcaps{$1}', + meta: 'classicthesis-cmd', + score: 0.002677188251799468 + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'classicthesis-cmd', + score: 0.005008938879210868 + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'classicthesis-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'classicthesis-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\part{}', + snippet: '\\part{$1}', + meta: 'classicthesis-cmd', + score: 0.022180129487444723 + }, + { + caption: '\\tocEntry{}', + snippet: '\\tocEntry{$1}', + meta: 'classicthesis-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\graffito{}', + snippet: '\\graffito{$1}', + meta: 'classicthesis-cmd', + score: 1.1006799670632527e-5 + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'classicthesis-cmd', + score: 0.422097569591803 + }, + { + caption: '\\spacedallcaps{}', + snippet: '\\spacedallcaps{$1}', + meta: 'classicthesis-cmd', + score: 0.0015281000475958944 + }, + { + caption: '\\cftchapleader', + snippet: '\\cftchapleader', + meta: 'classicthesis-cmd', + score: 1.0644172549700836e-5 + }, + { + caption: '\\myVersion', + snippet: '\\myVersion', + meta: 'classicthesis-cmd', + score: 0.00018029288638573757 + }, + { + caption: '\\ctparttext{}', + snippet: '\\ctparttext{$1}', + meta: 'classicthesis-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'classicthesis-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'classicthesis-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'classicthesis-cmd', + score: 0.004974385202605165 + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'classicthesis-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'classicthesis-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'classicthesis-cmd', + score: 0.04533364657852219 + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'classicthesis-cmd', + score: 0.07098077735912875 + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'classicthesis-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'classicthesis-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'classicthesis-cmd', + score: 0.059857788139528495 + }, + { + caption: '\\titleclass{}{}[]', + snippet: '\\titleclass{$1}{$2}[$3]', + meta: 'classicthesis-cmd', + score: 0.00028979763314974667 + }, + { + caption: '\\titlelabel{}', + snippet: '\\titlelabel{$1}', + meta: 'classicthesis-cmd', + score: 6.40387839367932e-6 + }, + { + caption: '\\thetitle', + snippet: '\\thetitle', + meta: 'classicthesis-cmd', + score: 0.0015531478302713473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'classicthesis-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'classicthesis-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\titleformat{}{}{}{}{}[]', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]', + meta: 'classicthesis-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}[]{}{}{}{}', + snippet: '\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'classicthesis-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}{}', + snippet: '\\titleformat{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}{}{}{}{}', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}', + meta: 'classicthesis-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titlespacing{}{}{}{}', + snippet: '\\titlespacing{$1}{$2}{$3}{$4}', + meta: 'classicthesis-cmd', + score: 0.023062744385192156 + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'classicthesis-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'classicthesis-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\filleft', + snippet: '\\filleft', + meta: 'classicthesis-cmd', + score: 7.959989906732799e-5 + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'classicthesis-cmd', + score: 0.0004835660211260246 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'classicthesis-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'classicthesis-cmd', + score: 0.044016804142963585 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'classicthesis-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\chaptertitlename', + snippet: '\\chaptertitlename', + meta: 'classicthesis-cmd', + score: 0.0016985007766926272 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'classicthesis-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\filright', + snippet: '\\filright', + meta: 'classicthesis-cmd', + score: 7.959989906732799e-5 + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'classicthesis-cmd', + score: 0.019273712561461216 + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'classicthesis-cmd', + score: 0.019273712561461216 + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.0008555564394100388 + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.012985816912639263 + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'classicthesis-cmd', + score: 0.000396664302361659 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'classicthesis-cmd', + score: 2.341195220791228 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'classicthesis-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'classicthesis-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'classicthesis-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'classicthesis-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'classicthesis-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'classicthesis-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'classicthesis-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'classicthesis-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\lsstyle', + snippet: '\\lsstyle', + meta: 'classicthesis-cmd', + score: 0.0023367519914345774 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'classicthesis-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\DisableLigatures[]{}', + snippet: '\\DisableLigatures[$1]{$2}', + meta: 'classicthesis-cmd', + score: 0.0009805246614299932 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'classicthesis-cmd', + score: 0.00021116765384691477 + } + ], + expl3: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'expl3-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'expl3-cmd', + score: 0.2864294797053033 + } + ], + 'pst-plot': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-plot-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-plot-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-plot-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-plot-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-plot-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-plot-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-plot-cmd', + score: 0.006520475264573554 + } + ], + chemarrow: [ + { + caption: '\\chemarrow', + snippet: '\\chemarrow', + meta: 'chemarrow-cmd', + score: 0.0005176077206367611 + } + ], + prettyref: [ + { + caption: '\\newrefformat{}{}', + snippet: '\\newrefformat{$1}{$2}', + meta: 'prettyref-cmd', + score: 0.001373625900102228 + }, + { + caption: '\\prettyref{}', + snippet: '\\prettyref{$1}', + meta: 'prettyref-cmd', + score: 0.005783541047730358 + } + ], + versions: [ + { + caption: '\\includeversion{}', + snippet: '\\includeversion{$1}', + meta: 'versions-cmd', + score: 0.0028410409433993543 + }, + { + caption: '\\excludeversion{}', + snippet: '\\excludeversion{$1}', + meta: 'versions-cmd', + score: 0.001742562336270228 + }, + { + caption: '\\processifversion{}{}', + snippet: '\\processifversion{$1}{$2}', + meta: 'versions-cmd', + score: 0.0022991412707353805 + } + ], + contour: [ + { + caption: '\\contour{}{}', + snippet: '\\contour{$1}{$2}', + meta: 'contour-cmd', + score: 0.0008245159401597211 + }, + { + caption: '\\contourlength{}', + snippet: '\\contourlength{$1}', + meta: 'contour-cmd', + score: 8.130187059343861e-5 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'contour-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'contour-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'contour-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'contour-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'contour-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'contour-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'contour-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'contour-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'contour-cmd', + score: 0.008565354665444157 + } + ], + xintexpr: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xintexpr-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xintexpr-cmd', + score: 0.021170869458413965 + } + ], + tocstyle: [ + { + caption: '\\usetocstyle{}', + snippet: '\\usetocstyle{$1}', + meta: 'tocstyle-cmd', + score: 3.2405622997778076e-6 + } + ], + bigdelim: [ + { + caption: '\\multirow{}{}{}', + snippet: '\\multirow{$1}{$2}{$3}', + meta: 'bigdelim-cmd', + score: 0.07525389638751734 + }, + { + caption: '\\multirow{}[]{}{}', + snippet: '\\multirow{$1}[$2]{$3}{$4}', + meta: 'bigdelim-cmd', + score: 0.07525389638751734 + } + ], + eulervm: [ + { + caption: '\\big', + snippet: '\\big', + meta: 'eulervm-cmd', + score: 0.05613164277964739 + } + ], + xr: [ + { + caption: '\\externaldocument{}', + snippet: '\\externaldocument{$1}', + meta: 'xr-cmd', + score: 0.0008648763879096798 + } + ], + yhmath: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'yhmath-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'yhmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'yhmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'yhmath-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'yhmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'yhmath-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'yhmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'yhmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'yhmath-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'yhmath-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'yhmath-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'yhmath-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'yhmath-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'yhmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'yhmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'yhmath-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'yhmath-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'yhmath-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'yhmath-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'yhmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'yhmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'yhmath-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'yhmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'yhmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'yhmath-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'yhmath-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'yhmath-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'yhmath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'yhmath-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'yhmath-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'yhmath-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'yhmath-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'yhmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'yhmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'yhmath-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'yhmath-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'yhmath-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'yhmath-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'yhmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'yhmath-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'yhmath-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'yhmath-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'yhmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'yhmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'yhmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'yhmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'yhmath-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'yhmath-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'yhmath-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'yhmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'yhmath-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'yhmath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'yhmath-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'yhmath-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'yhmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'yhmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'yhmath-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'yhmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'yhmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'yhmath-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'yhmath-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'yhmath-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'yhmath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'yhmath-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'yhmath-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'yhmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'yhmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'yhmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'yhmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'yhmath-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'yhmath-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'yhmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'yhmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'yhmath-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'yhmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'yhmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'yhmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'yhmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'yhmath-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'yhmath-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'yhmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'yhmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'yhmath-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'yhmath-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'yhmath-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'yhmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'yhmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'yhmath-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'yhmath-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'yhmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'yhmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'yhmath-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'yhmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'yhmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'yhmath-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'yhmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'yhmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'yhmath-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'yhmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'yhmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'yhmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'yhmath-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'yhmath-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'yhmath-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'yhmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'yhmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'yhmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'yhmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'yhmath-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'yhmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'yhmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'yhmath-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'yhmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'yhmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'yhmath-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'yhmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'yhmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'yhmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'yhmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'yhmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'yhmath-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'yhmath-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'yhmath-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'yhmath-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'yhmath-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'yhmath-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'yhmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'yhmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'yhmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'yhmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'yhmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'yhmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'yhmath-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'yhmath-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'yhmath-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'yhmath-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'yhmath-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'yhmath-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'yhmath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'yhmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'yhmath-cmd', + score: 0.0063276692758974925 + } + ], + XCharter: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'XCharter-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'XCharter-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'XCharter-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'XCharter-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'XCharter-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'XCharter-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'XCharter-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'XCharter-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'XCharter-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'XCharter-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'XCharter-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'XCharter-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'XCharter-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'XCharter-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'XCharter-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'XCharter-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'XCharter-cmd', + score: 0.008565354665444157 + } + ], + 'tikz-feynman': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-feynman-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-feynman-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-feynman-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-feynman-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-feynman-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-feynman-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-feynman-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-feynman-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-feynman-cmd', + score: 0.2864294797053033 + } + ], + easylist: [ + { + caption: '\\ListProperties', + snippet: '\\ListProperties', + meta: 'easylist-cmd', + score: 5.7747123038330224e-5 + } + ], + hologo: [ + { + caption: '\\hologo{}', + snippet: '\\hologo{$1}', + meta: 'hologo-cmd', + score: 0.00028086100750460613 + } + ], + cases: [ + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'cases-cmd', + score: 0.002995924112493351 + } + ], + xint: [ + { + caption: '\\xintSgnFork{}', + snippet: '\\xintSgnFork{$1}', + meta: 'xint-cmd', + score: 0.0005720629946669665 + }, + { + caption: '\\xintCmp{}{}', + snippet: '\\xintCmp{$1}{$2}', + meta: 'xint-cmd', + score: 0.0002860314973334833 + }, + { + caption: '\\xintOdd{}', + snippet: '\\xintOdd{$1}', + meta: 'xint-cmd', + score: 0.0002860314973334833 + }, + { + caption: '\\xintGeq', + snippet: '\\xintGeq', + meta: 'xint-cmd', + score: 0.0002860314973334833 + } + ], + inputenx: [ + { + caption: '\\inputencoding{}', + snippet: '\\inputencoding{$1}', + meta: 'inputenx-cmd', + score: 0.0002447047447770061 + } + ], + vwcol: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'vwcol-cmd', + score: 0.04598628699063736 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'vwcol-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\justifying', + snippet: '\\justifying', + meta: 'vwcol-cmd', + score: 0.010373702256548788 + }, + { + caption: '\\justifying{}', + snippet: '\\justifying{$1}', + meta: 'vwcol-cmd', + score: 0.010373702256548788 + }, + { + caption: '\\RaggedRight', + snippet: '\\RaggedRight', + meta: 'vwcol-cmd', + score: 0.001021021782267457 + }, + { + caption: '\\Centering', + snippet: '\\Centering', + meta: 'vwcol-cmd', + score: 0.00037395241488843035 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'vwcol-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'vwcol-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'vwcol-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'vwcol-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'vwcol-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'vwcol-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'vwcol-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'vwcol-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'vwcol-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'vwcol-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'vwcol-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'vwcol-cmd', + score: 0.021170869458413965 + } + ], + multimedia: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'multimedia-cmd', + score: 0.00037306820619479756 + } + ], + sgame: [ + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'sgame-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'sgame-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'sgame-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'sgame-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'sgame-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'sgame-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'sgame-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'sgame-cmd', + score: 0.2864294797053033 + } + ], + bussproofs: [ + { + caption: '\\makeatletter', + snippet: '\\makeatletter', + meta: 'bussproofs-cmd', + score: 0.041979363643201636 + }, + { + caption: '\\makeatother', + snippet: '\\makeatother', + meta: 'bussproofs-cmd', + score: 0.03923442255397878 + } + ], + titlepic: [ + { + caption: '\\titlepic{}', + snippet: '\\titlepic{$1}', + meta: 'titlepic-cmd', + score: 0.00020896323441399082 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'titlepic-cmd', + score: 0.7504160124360846 + } + ], + paracol: [ + { + caption: '\\switchcolumn', + snippet: '\\switchcolumn', + meta: 'paracol-cmd', + score: 0.0008273060639466222 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'paracol-cmd', + score: 0.008565354665444157 + } + ], + polyglossia: [ + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'polyglossia-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'polyglossia-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\normalfont', + snippet: '\\normalfont', + meta: 'polyglossia-cmd', + score: 0.06871177093091137 + }, + { + caption: '\\normalfont{}', + snippet: '\\normalfont{$1}', + meta: 'polyglossia-cmd', + score: 0.06871177093091137 + }, + { + caption: '\\setdefaultlanguage{}', + snippet: '\\setdefaultlanguage{$1}', + meta: 'polyglossia-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'polyglossia-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'polyglossia-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'polyglossia-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'polyglossia-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'polyglossia-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'polyglossia-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'polyglossia-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'polyglossia-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'polyglossia-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'polyglossia-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'polyglossia-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'polyglossia-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'polyglossia-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'polyglossia-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'polyglossia-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'polyglossia-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'polyglossia-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'polyglossia-cmd', + score: 0.2864294797053033 + } + ], + 'zref-user': [ + { + caption: '\\zlabel{}', + snippet: '\\zlabel{$1}', + meta: 'zref-user-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\zref', + snippet: '\\zref', + meta: 'zref-user-cmd', + score: 0.002193637536912482 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'zref-user-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'zref-user-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-user-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-user-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-user-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-user-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927 + } + ], + 'zref-abspage': [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'zref-abspage-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'zref-abspage-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'zref-abspage-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'zref-abspage-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-abspage-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-abspage-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927 + } + ], + quotchap: [ + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'quotchap-cmd', + score: 0.422097569591803 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'quotchap-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'quotchap-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\qauthor{}', + snippet: '\\qauthor{$1}', + meta: 'quotchap-cmd', + score: 0.002335082759143631 + } + ], + misccorr: [ + { + caption: '\\subsection{}', + snippet: '\\subsection{$1}', + meta: 'misccorr-cmd', + score: 1.3890912739512353 + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'misccorr-cmd', + score: 3.0952612541683835 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'misccorr-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\makelabel', + snippet: '\\makelabel', + meta: 'misccorr-cmd', + score: 5.739925426740175e-5 + }, + { + caption: '\\makelabel{}', + snippet: '\\makelabel{$1}', + meta: 'misccorr-cmd', + score: 5.739925426740175e-5 + }, + { + caption: '\\makelabel[]{}', + snippet: '\\makelabel[$1]{$2}', + meta: 'misccorr-cmd', + score: 5.739925426740175e-5 + }, + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'misccorr-cmd', + score: 0.0017966000518546787 + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'misccorr-cmd', + score: 0.025060530944368123 + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'misccorr-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'misccorr-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'misccorr-cmd', + score: 0.0006671850995492977 + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'misccorr-cmd', + score: 0.0006671850995492977 + } + ], + academicons: [ + { + caption: '\\aiResearchGateSquare', + snippet: '\\aiResearchGateSquare', + meta: 'academicons-cmd', + score: 0.0005747853176838451 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'academicons-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'academicons-cmd', + score: 0.2864294797053033 + } + ], + tasks: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tasks-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tasks-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tasks-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tasks-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tasks-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tasks-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tasks-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tasks-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tasks-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tasks-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tasks-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tasks-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tasks-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tasks-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tasks-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tasks-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tasks-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tasks-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tasks-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tasks-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tasks-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tasks-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tasks-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tasks-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tasks-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tasks-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tasks-cmd', + score: 0.2864294797053033 + } + ], + 'pstricks-add': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pstricks-add-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pstricks-add-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pstricks-add-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pstricks-add-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pstricks-add-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pstricks-add-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pstricks-add-cmd', + score: 0.006520475264573554 + } + ], + extramarks: [ + { + caption: '\\leftmark', + snippet: '\\leftmark', + meta: 'extramarks-cmd', + score: 0.01094124445235767 + }, + { + caption: '\\extramarks{}{}', + snippet: '\\extramarks{$1}{$2}', + meta: 'extramarks-cmd', + score: 0.0003269562507660904 + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'extramarks-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'extramarks-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'extramarks-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'extramarks-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\rightmark', + snippet: '\\rightmark', + meta: 'extramarks-cmd', + score: 0.008472328846194114 + } + ], + calrsfs: [ + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'calrsfs-cmd', + score: 0.35084018920966636 + } + ], + newlfont: [ + { + caption: '\\em', + snippet: '\\em', + meta: 'newlfont-cmd', + score: 0.10357353994640862 + } + ], + mdwtab: [ + { + caption: '\\cline{}', + snippet: '\\cline{$1}', + meta: 'mdwtab-cmd', + score: 0.07276573550543858 + }, + { + caption: '\\hline', + snippet: '\\hline', + meta: 'mdwtab-cmd', + score: 1.3209538327406387 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'mdwtab-cmd', + score: 0.5473606021405326 + } + ], + mdwmath: [ + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mdwmath-cmd', + score: 0.04318078602869565 + } + ], + wallpaper: [ + { + caption: '\\CenterWallPaper{}{}', + snippet: '\\CenterWallPaper{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.00042983945496357105 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'wallpaper-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wallpaper-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wallpaper-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'wallpaper-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'wallpaper-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'wallpaper-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wallpaper-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wallpaper-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'wallpaper-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'wallpaper-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'wallpaper-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'wallpaper-cmd', + score: 0.000325977535138643 + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'wallpaper-cmd', + score: 0.0008957666085644653 + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'wallpaper-cmd', + score: 0.0003608141410278152 + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'wallpaper-cmd', + score: 0.0007216282820556304 + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'wallpaper-cmd', + score: 0.0017658629469099734 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wallpaper-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wallpaper-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'wallpaper-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'wallpaper-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'wallpaper-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'wallpaper-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'wallpaper-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'wallpaper-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'wallpaper-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'wallpaper-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'wallpaper-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'wallpaper-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'wallpaper-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'wallpaper-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'wallpaper-cmd', + score: 0.008565354665444157 + } + ], + newunicodechar: [ + { + caption: '\\newunicodechar{}{}', + snippet: '\\newunicodechar{$1}{$2}', + meta: 'newunicodechar-cmd', + score: 8.718084183564492e-5 + } + ], + thmtools: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thmtools-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\listtheoremname', + snippet: '\\listtheoremname', + meta: 'thmtools-cmd', + score: 1.9443373798666845e-5 + }, + { + caption: '\\thmtformatoptarg', + snippet: '\\thmtformatoptarg', + meta: 'thmtools-cmd', + score: 6.353668036093916e-5 + }, + { + caption: '\\listoftheorems[]', + snippet: '\\listoftheorems[$1]', + meta: 'thmtools-cmd', + score: 1.9443373798666845e-5 + }, + { + caption: '\\declaretheoremstyle[]{}', + snippet: '\\declaretheoremstyle[$1]{$2}', + meta: 'thmtools-cmd', + score: 0.0001168034231635369 + }, + { + caption: '\\declaretheorem[]{}', + snippet: '\\declaretheorem[$1]{$2}', + meta: 'thmtools-cmd', + score: 0.0004904790216915127 + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'thmtools-cmd', + score: 0.02533412165007986 + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thmtools-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thmtools-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thmtools-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thmtools-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thmtools-cmd', + score: 0.215689795055434 + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thmtools-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thmtools-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thmtools-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thmtools-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thmtools-cmd', + score: 0.008565354665444157 + } + ], + nccmath: [ + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'nccmath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'nccmath-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'nccmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'nccmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'nccmath-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'nccmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'nccmath-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'nccmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'nccmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'nccmath-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'nccmath-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'nccmath-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'nccmath-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'nccmath-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'nccmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'nccmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'nccmath-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'nccmath-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'nccmath-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'nccmath-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'nccmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'nccmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'nccmath-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'nccmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'nccmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'nccmath-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'nccmath-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'nccmath-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'nccmath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'nccmath-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'nccmath-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'nccmath-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'nccmath-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'nccmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'nccmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'nccmath-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'nccmath-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'nccmath-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'nccmath-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'nccmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'nccmath-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'nccmath-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'nccmath-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'nccmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'nccmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'nccmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'nccmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'nccmath-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'nccmath-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'nccmath-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'nccmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'nccmath-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'nccmath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'nccmath-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'nccmath-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'nccmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'nccmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'nccmath-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'nccmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'nccmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'nccmath-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'nccmath-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'nccmath-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'nccmath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'nccmath-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'nccmath-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'nccmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'nccmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'nccmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'nccmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'nccmath-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'nccmath-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'nccmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'nccmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'nccmath-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'nccmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'nccmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'nccmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'nccmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'nccmath-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'nccmath-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'nccmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'nccmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'nccmath-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'nccmath-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'nccmath-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'nccmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'nccmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'nccmath-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'nccmath-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'nccmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'nccmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'nccmath-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'nccmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'nccmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'nccmath-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'nccmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'nccmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'nccmath-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'nccmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'nccmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'nccmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'nccmath-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'nccmath-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'nccmath-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'nccmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'nccmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'nccmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'nccmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'nccmath-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'nccmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'nccmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'nccmath-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'nccmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'nccmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'nccmath-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'nccmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'nccmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'nccmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'nccmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'nccmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'nccmath-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'nccmath-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'nccmath-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'nccmath-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'nccmath-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'nccmath-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'nccmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'nccmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'nccmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'nccmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'nccmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'nccmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'nccmath-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'nccmath-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'nccmath-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'nccmath-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'nccmath-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'nccmath-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nccmath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'nccmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'nccmath-cmd', + score: 0.0063276692758974925 + } + ], + scrtime: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrtime-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrtime-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrtime-cmd', + score: 0.0008555564394100388 + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrtime-cmd', + score: 0.012985816912639263 + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrtime-cmd', + score: 0.000396664302361659 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrtime-cmd', + score: 0.00037306820619479756 + } + ], + luainputenc: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'luainputenc-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'luainputenc-cmd', + score: 0.008565354665444157 + } + ], + curve2e: [ + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'curve2e-cmd', + score: 0.00022468880600368487 + }, + { + caption: '\\put', + snippet: '\\put', + meta: 'curve2e-cmd', + score: 0.0406766030275089 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'curve2e-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'curve2e-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'curve2e-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'curve2e-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'curve2e-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'curve2e-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'curve2e-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'curve2e-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\Line', + snippet: '\\Line', + meta: 'curve2e-cmd', + score: 0.0006078790177929149 + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'curve2e-cmd', + score: 0.0008987552240147395 + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'curve2e-cmd', + score: 0.014519741542622297 + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'curve2e-cmd', + score: 0.00022468880600368487 + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'curve2e-cmd', + score: 0.002970308722584179 + } + ], + couriers: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'couriers-cmd', + score: 0.00037306820619479756 + } + ], + caption3: [ + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'caption3-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'caption3-cmd', + score: 0.0003890810058478364 + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'caption3-cmd', + score: 0.0004717618449370015 + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'caption3-cmd', + score: 5.0133404990680195e-5 + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'caption3-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'caption3-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'caption3-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption3-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption3-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption3-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'caption3-cmd', + score: 0.00015256647321237863 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption3-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'caption3-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'caption3-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'caption3-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'caption3-cmd', + score: 0.00037306820619479756 + } + ], + gauss: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'gauss-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'gauss-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'gauss-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'gauss-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'gauss-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'gauss-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'gauss-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'gauss-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'gauss-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'gauss-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'gauss-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'gauss-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'gauss-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'gauss-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'gauss-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'gauss-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'gauss-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'gauss-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'gauss-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'gauss-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'gauss-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'gauss-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'gauss-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'gauss-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'gauss-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'gauss-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'gauss-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'gauss-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'gauss-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'gauss-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'gauss-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'gauss-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'gauss-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'gauss-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'gauss-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'gauss-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'gauss-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'gauss-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'gauss-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'gauss-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'gauss-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'gauss-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'gauss-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'gauss-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'gauss-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'gauss-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'gauss-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'gauss-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'gauss-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'gauss-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'gauss-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'gauss-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'gauss-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'gauss-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'gauss-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'gauss-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'gauss-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'gauss-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'gauss-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'gauss-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'gauss-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'gauss-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'gauss-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'gauss-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'gauss-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'gauss-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'gauss-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'gauss-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'gauss-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'gauss-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'gauss-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'gauss-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'gauss-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'gauss-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'gauss-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'gauss-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'gauss-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'gauss-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'gauss-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'gauss-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'gauss-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'gauss-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'gauss-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'gauss-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'gauss-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'gauss-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'gauss-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'gauss-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'gauss-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'gauss-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'gauss-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'gauss-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'gauss-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'gauss-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'gauss-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'gauss-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'gauss-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'gauss-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'gauss-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'gauss-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'gauss-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'gauss-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'gauss-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'gauss-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'gauss-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'gauss-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'gauss-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'gauss-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'gauss-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'gauss-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'gauss-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'gauss-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'gauss-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'gauss-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'gauss-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'gauss-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'gauss-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'gauss-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'gauss-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'gauss-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'gauss-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'gauss-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'gauss-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'gauss-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'gauss-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'gauss-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'gauss-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'gauss-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'gauss-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'gauss-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'gauss-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'gauss-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'gauss-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'gauss-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'gauss-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'gauss-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'gauss-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'gauss-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'gauss-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'gauss-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'gauss-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'gauss-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'gauss-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'gauss-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'gauss-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'gauss-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'gauss-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'gauss-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gauss-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'gauss-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'gauss-cmd', + score: 0.0063276692758974925 + } + ], + fancyref: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fancyref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fancyref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fancyref-cmd', + score: 0.008565354665444157 + } + ], + eufrak: [ + { + caption: '\\mathfrak{}', + snippet: '\\mathfrak{$1}', + meta: 'eufrak-cmd', + score: 0.025213895825856578 + }, + { + caption: '\\mathfrak', + snippet: '\\mathfrak', + meta: 'eufrak-cmd', + score: 0.025213895825856578 + } + ], + fixme: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'fixme-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'fixme-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'fixme-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'fixme-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'fixme-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'fixme-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'fixme-cmd', + score: 0.0022216421267780076 + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'fixme-cmd', + score: 0.0072203369120285256 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fixme-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fixme-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'fixme-cmd', + score: 0.413853376001159 + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'fixme-cmd', + score: 0.0024547099784948665 + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'fixme-cmd', + score: 0.0024547099784948665 + } + ], + 'pgf-umlsd': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlsd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgf-umlsd-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgf-umlsd-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlsd-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlsd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgf-umlsd-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlsd-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlsd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.2864294797053033 + } + ], + tgadventor: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgadventor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgadventor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgadventor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgadventor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgadventor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgadventor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgadventor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgadventor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgadventor-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgadventor-cmd', + score: 0.021170869458413965 + } + ], + fancyheadings: [ + { + caption: '\\lhead{}', + snippet: '\\lhead{$1}', + meta: 'fancyheadings-cmd', + score: 0.05268978171228714 + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'fancyheadings-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'fancyheadings-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\fancypagestyle{}{}', + snippet: '\\fancypagestyle{$1}{$2}', + meta: 'fancyheadings-cmd', + score: 0.009430919590937878 + }, + { + caption: '\\footrule', + snippet: '\\footrule', + meta: 'fancyheadings-cmd', + score: 0.0010032754348913366 + }, + { + caption: '\\footrule{}', + snippet: '\\footrule{$1}', + meta: 'fancyheadings-cmd', + score: 0.0010032754348913366 + }, + { + caption: '\\fancyfoot[]{}', + snippet: '\\fancyfoot[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.024973618823189894 + }, + { + caption: '\\fancyfoot{}', + snippet: '\\fancyfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.024973618823189894 + }, + { + caption: '\\fancyfootoffset[]{}', + snippet: '\\fancyfootoffset[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.0015373246231684555 + }, + { + caption: '\\fancyfootoffset{}', + snippet: '\\fancyfootoffset{$1}', + meta: 'fancyheadings-cmd', + score: 0.0015373246231684555 + }, + { + caption: '\\footruleskip', + snippet: '\\footruleskip', + meta: 'fancyheadings-cmd', + score: 0.000830117957327721 + }, + { + caption: '\\fancyheadoffset[]{}', + snippet: '\\fancyheadoffset[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.0016786568695309166 + }, + { + caption: '\\fancyheadoffset{}', + snippet: '\\fancyheadoffset{$1}', + meta: 'fancyheadings-cmd', + score: 0.0016786568695309166 + }, + { + caption: '\\iffloatpage{}{}', + snippet: '\\iffloatpage{$1}{$2}', + meta: 'fancyheadings-cmd', + score: 6.606286310833368e-5 + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.013411641301057813 + }, + { + caption: '\\subsectionmark', + snippet: '\\subsectionmark', + meta: 'fancyheadings-cmd', + score: 3.1153423008593836e-5 + }, + { + caption: '\\footrulewidth', + snippet: '\\footrulewidth', + meta: 'fancyheadings-cmd', + score: 0.011424740897486949 + }, + { + caption: '\\fancyhfoffset[]{}', + snippet: '\\fancyhfoffset[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 3.741978601121172e-5 + }, + { + caption: '\\rhead{}', + snippet: '\\rhead{$1}', + meta: 'fancyheadings-cmd', + score: 0.022782817416731292 + }, + { + caption: '\\fancyplain{}{}', + snippet: '\\fancyplain{$1}{$2}', + meta: 'fancyheadings-cmd', + score: 0.007402339896386138 + }, + { + caption: '\\rfoot{}', + snippet: '\\rfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.013393817825547868 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fancyheadings-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\plainheadrulewidth', + snippet: '\\plainheadrulewidth', + meta: 'fancyheadings-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'fancyheadings-cmd', + score: 0.03225350148161425 + }, + { + caption: '\\lfoot{}', + snippet: '\\lfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.00789399846642229 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'fancyheadings-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'fancyheadings-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\fancyhf{}', + snippet: '\\fancyhf{$1}', + meta: 'fancyheadings-cmd', + score: 0.02314618933449356 + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'fancyheadings-cmd', + score: 0.005008938879210868 + }, + { + caption: '\\fancyhead[]{}', + snippet: '\\fancyhead[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.039101068064744296 + }, + { + caption: '\\fancyhead{}', + snippet: '\\fancyhead{$1}', + meta: 'fancyheadings-cmd', + score: 0.039101068064744296 + }, + { + caption: '\\nouppercase{}', + snippet: '\\nouppercase{$1}', + meta: 'fancyheadings-cmd', + score: 0.006416387071584083 + }, + { + caption: '\\nouppercase', + snippet: '\\nouppercase', + meta: 'fancyheadings-cmd', + score: 0.006416387071584083 + }, + { + caption: '\\headrule', + snippet: '\\headrule', + meta: 'fancyheadings-cmd', + score: 0.0008327432627715623 + }, + { + caption: '\\headrule{}', + snippet: '\\headrule{$1}', + meta: 'fancyheadings-cmd', + score: 0.0008327432627715623 + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'fancyheadings-cmd', + score: 0.00755042164734884 + }, + { + caption: '\\headrulewidth', + snippet: '\\headrulewidth', + meta: 'fancyheadings-cmd', + score: 0.02268137935335823 + } + ], + 'tikz-3dplot': [ + { + caption: '\\tdplotsetmaincoords{}{}', + snippet: '\\tdplotsetmaincoords{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.00021728148272883815 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-3dplot-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-3dplot-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-3dplot-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-3dplot-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-3dplot-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-3dplot-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-3dplot-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-3dplot-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.2864294797053033 + } + ], + ltxtable: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'ltxtable-cmd', + score: 0.03789745970461662 + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ltxtable-cmd', + score: 0.0008038857295393196 + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ltxtable-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ltxtable-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ltxtable-cmd', + score: 0.0005861357565780464 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltxtable-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ltxtable-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ltxtable-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ltxtable-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltxtable-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ltxtable-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ltxtable-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ltxtable-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ltxtable-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'ltxtable-cmd', + score: 0.0023853501147448834 + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'ltxtable-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ltxtable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ltxtable-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'ltxtable-cmd', + score: 9.952664522415981e-5 + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'ltxtable-cmd', + score: 0.0016148498709822416 + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'ltxtable-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'ltxtable-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'ltxtable-cmd', + score: 0.0029238994233674776 + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'ltxtable-cmd', + score: 0.0313525090421608 + } + ], + pict2e: [ + { + caption: '\\Line', + snippet: '\\Line', + meta: 'pict2e-cmd', + score: 0.0006078790177929149 + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'pict2e-cmd', + score: 0.0008987552240147395 + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'pict2e-cmd', + score: 0.014519741542622297 + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'pict2e-cmd', + score: 0.00022468880600368487 + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'pict2e-cmd', + score: 0.002970308722584179 + } + ], + ltablex: [ + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'ltablex-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ltablex-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ltablex-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ltablex-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltablex-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ltablex-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ltablex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ltablex-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ltablex-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'ltablex-cmd', + score: 0.03789745970461662 + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ltablex-cmd', + score: 0.0008038857295393196 + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ltablex-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ltablex-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ltablex-cmd', + score: 0.0005861357565780464 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltablex-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'ltablex-cmd', + score: 0.0023853501147448834 + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'ltablex-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ltablex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ltablex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'ltablex-cmd', + score: 9.952664522415981e-5 + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'ltablex-cmd', + score: 0.0016148498709822416 + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'ltablex-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'ltablex-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'ltablex-cmd', + score: 0.0029238994233674776 + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'ltablex-cmd', + score: 0.0313525090421608 + } + ], + amsopn: [ + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'amsopn-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'amsopn-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'amsopn-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'amsopn-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'amsopn-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'amsopn-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'amsopn-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'amsopn-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'amsopn-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'amsopn-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'amsopn-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'amsopn-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'amsopn-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'amsopn-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'amsopn-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'amsopn-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'amsopn-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'amsopn-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'amsopn-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'amsopn-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'amsopn-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'amsopn-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'amsopn-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'amsopn-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'amsopn-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'amsopn-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'amsopn-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'amsopn-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'amsopn-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'amsopn-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'amsopn-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'amsopn-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'amsopn-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'amsopn-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'amsopn-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'amsopn-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'amsopn-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'amsopn-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'amsopn-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'amsopn-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'amsopn-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'amsopn-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'amsopn-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'amsopn-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'amsopn-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'amsopn-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'amsopn-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'amsopn-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'amsopn-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'amsopn-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'amsopn-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'amsopn-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsopn-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsopn-cmd', + score: 0.0063276692758974925 + } + ], + topcoman: [ + { + caption: '\\listing{}', + snippet: '\\listing{$1}', + meta: 'topcoman-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\micro', + snippet: '\\micro', + meta: 'topcoman-cmd', + score: 0.011051971930487929 + }, + { + caption: '\\gradi', + snippet: '\\gradi', + meta: 'topcoman-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\unit[]{}', + snippet: '\\unit[$1]{$2}', + meta: 'topcoman-cmd', + score: 0.028299796173135428 + }, + { + caption: '\\unit{}', + snippet: '\\unit{$1}', + meta: 'topcoman-cmd', + score: 0.028299796173135428 + }, + { + caption: '\\ped{}', + snippet: '\\ped{$1}', + meta: 'topcoman-cmd', + score: 0.0007129548652040002 + }, + { + caption: '\\ohm', + snippet: '\\ohm', + meta: 'topcoman-cmd', + score: 0.0038146685721293138 + }, + { + caption: '\\gei', + snippet: '\\gei', + meta: 'topcoman-cmd', + score: 0.00023765162173466673 + } + ], + topfront: [ + { + caption: '\\corsodilaurea{}', + snippet: '\\corsodilaurea{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NomeQuartoTomo{}', + snippet: '\\NomeQuartoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\ciclodidottorato{}', + snippet: '\\ciclodidottorato{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\CorsoDiLaureaIn{}', + snippet: '\\CorsoDiLaureaIn{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\ateneo{}', + snippet: '\\ateneo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\retrofrontespizio{}', + snippet: '\\retrofrontespizio{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\InName{}', + snippet: '\\InName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\secondocandidato{}', + snippet: '\\secondocandidato{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NomeMonografia{}', + snippet: '\\NomeMonografia{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NomeTutoreAziendale{}', + snippet: '\\NomeTutoreAziendale{$1}', + meta: 'topfront-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\TutorName{}', + snippet: '\\TutorName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NomeDissertazione{}', + snippet: '\\NomeDissertazione{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\sedutadilaurea{}', + snippet: '\\sedutadilaurea{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\logosede{}', + snippet: '\\logosede{$1}', + meta: 'topfront-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\TesiDiLaurea{}', + snippet: '\\TesiDiLaurea{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NomeTerzoTomo{}', + snippet: '\\NomeTerzoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\AdvisorName{}', + snippet: '\\AdvisorName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\facolta[]{}', + snippet: '\\facolta[$1]{$2}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\CycleName{}', + snippet: '\\CycleName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NomePrimoTomo{}', + snippet: '\\NomePrimoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\candidato{}', + snippet: '\\candidato{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NomeSecondoTomo{}', + snippet: '\\NomeSecondoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\titolo{}', + snippet: '\\titolo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\CandidateName{}', + snippet: '\\CandidateName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\secondorelatore{}', + snippet: '\\secondorelatore{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\FacoltaDi{}', + snippet: '\\FacoltaDi{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\nomeateneo{}', + snippet: '\\nomeateneo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\DottoratoIn{}', + snippet: '\\DottoratoIn{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\sottotitolo{}', + snippet: '\\sottotitolo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\relatore{}', + snippet: '\\relatore{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\tutoreaziendale{}', + snippet: '\\tutoreaziendale{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673 + } + ], + mathspec: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'mathspec-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mathspec-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mathspec-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'mathspec-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'mathspec-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mathspec-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'mathspec-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathspec-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'mathspec-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'mathspec-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'mathspec-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'mathspec-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'mathspec-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mathspec-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'mathspec-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'mathspec-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mathspec-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mathspec-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mathspec-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'mathspec-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mathspec-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathspec-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'mathspec-cmd', + score: 0.0063276692758974925 + } + ], + overpic: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'overpic-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'overpic-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'overpic-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'overpic-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'overpic-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'overpic-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'overpic-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'overpic-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'overpic-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'overpic-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'overpic-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'overpic-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'overpic-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'overpic-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'overpic-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'overpic-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'overpic-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'overpic-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'overpic-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'overpic-cmd', + score: 0.004719094298848707 + } + ], + 'tkz-euclide': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-euclide-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0018653410309739879 + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-euclide-cmd', + score: 0.00031058155311734754 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-euclide-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-euclide-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-euclide-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-euclide-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-euclide-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-euclide-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-euclide-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-euclide-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-euclide-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-euclide-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-euclide-cmd', + score: 0.2864294797053033 + } + ], + morewrites: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'morewrites-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'morewrites-cmd', + score: 0.2864294797053033 + } + ], + pgflibraryshapes: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgflibraryshapes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgflibraryshapes-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryshapes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryshapes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgflibraryshapes-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryshapes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryshapes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryshapes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.2864294797053033 + } + ], + pdfcolparallel: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdfcolparallel-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'pdfcolparallel-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'pdfcolparallel-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfcolparallel-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pdfcolparallel-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\ParallelRText{}', + snippet: '\\ParallelRText{$1}', + meta: 'pdfcolparallel-cmd', + score: 0.0005986518360651812 + }, + { + caption: '\\ParallelLText{}', + snippet: '\\ParallelLText{$1}', + meta: 'pdfcolparallel-cmd', + score: 0.0005986518360651812 + }, + { + caption: '\\ParallelPar', + snippet: '\\ParallelPar', + meta: 'pdfcolparallel-cmd', + score: 0.0005986518360651812 + } + ], + aeguill: [ + { + caption: '\\guillemotleft', + snippet: '\\guillemotleft', + meta: 'aeguill-cmd', + score: 9.764370963946686e-5 + }, + { + caption: '\\guillemotright', + snippet: '\\guillemotright', + meta: 'aeguill-cmd', + score: 9.764370963946686e-5 + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'aeguill-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'aeguill-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aeguill-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aeguill-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aeguill-cmd', + score: 0.021170869458413965 + } + ], + changes: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'changes-cmd', + score: 0.04598628699063736 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'changes-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'changes-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'changes-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'changes-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'changes-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'changes-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'changes-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'changes-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'changes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'changes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'changes-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'changes-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'changes-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'changes-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'changes-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'changes-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'changes-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'changes-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'changes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'changes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'changes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'changes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'changes-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'changes-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'changes-cmd', + score: 0.2864294797053033 + } + ], + droidmono: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'droidmono-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'droidmono-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'droidmono-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'droidmono-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'droidmono-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'droidmono-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\scshape', + snippet: '\\scshape', + meta: 'droidmono-cmd', + score: 0.05364108855914402 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'droidmono-cmd', + score: 0.00037306820619479756 + } + ], + tgheros: [ + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'tgheros-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'tgheros-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgheros-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgheros-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgheros-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgheros-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgheros-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgheros-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgheros-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgheros-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgheros-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgheros-cmd', + score: 0.021170869458413965 + } + ], + har2nat: [ + { + caption: '\\citeasnoun{}', + snippet: '\\citeasnoun{$1}', + meta: 'har2nat-cmd', + score: 0.010452591644582749 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'har2nat-cmd', + score: 2.341195220791228 + }, + { + caption: '\\citealt{}', + snippet: '\\citealt{$1}', + meta: 'har2nat-cmd', + score: 0.007302105441724955 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'har2nat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'har2nat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'har2nat-cmd', + score: 0.05216393882408519 + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'har2nat-cmd', + score: 0.04990693820960752 + }, + { + caption: '\\bibname', + snippet: '\\bibname', + meta: 'har2nat-cmd', + score: 0.007599529252128519 + }, + { + caption: '\\bibname{}', + snippet: '\\bibname{$1}', + meta: 'har2nat-cmd', + score: 0.007599529252128519 + }, + { + caption: '\\bibpunct', + snippet: '\\bibpunct', + meta: 'har2nat-cmd', + score: 0.001148574749873469 + }, + { + caption: '\\bibpunct{}{}{}{}{}{}', + snippet: '\\bibpunct{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'har2nat-cmd', + score: 0.001148574749873469 + }, + { + caption: '\\bibpunct[]{}{}{}{}{}{}', + snippet: '\\bibpunct[$1]{$2}{$3}{$4}{$5}{$6}{$7}', + meta: 'har2nat-cmd', + score: 0.001148574749873469 + }, + { + caption: '\\citepalias{}', + snippet: '\\citepalias{$1}', + meta: 'har2nat-cmd', + score: 0.00032712684909035603 + }, + { + caption: '\\citepalias[][]{}', + snippet: '\\citepalias[$1][$2]{$3}', + meta: 'har2nat-cmd', + score: 0.00032712684909035603 + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'har2nat-cmd', + score: 0.010304996748556729 + }, + { + caption: '\\citep{}', + snippet: '\\citep{$1}', + meta: 'har2nat-cmd', + score: 0.2941882834697057 + }, + { + caption: '\\bibsection', + snippet: '\\bibsection', + meta: 'har2nat-cmd', + score: 0.00038872734530908233 + }, + { + caption: '\\bibsection{}', + snippet: '\\bibsection{$1}', + meta: 'har2nat-cmd', + score: 0.00038872734530908233 + }, + { + caption: '\\refname', + snippet: '\\refname', + meta: 'har2nat-cmd', + score: 0.006490238196722249 + }, + { + caption: '\\refname{}', + snippet: '\\refname{$1}', + meta: 'har2nat-cmd', + score: 0.006490238196722249 + }, + { + caption: '\\citealp{}', + snippet: '\\citealp{$1}', + meta: 'har2nat-cmd', + score: 0.005275912376595364 + }, + { + caption: '\\citealp[]{}', + snippet: '\\citealp[$1]{$2}', + meta: 'har2nat-cmd', + score: 0.005275912376595364 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'har2nat-cmd', + score: 2.341195220791228 + }, + { + caption: '\\citetalias{}', + snippet: '\\citetalias{$1}', + meta: 'har2nat-cmd', + score: 0.001419571355756266 + }, + { + caption: '\\bibitem{}', + snippet: '\\bibitem{$1}', + meta: 'har2nat-cmd', + score: 0.3689547570562042 + }, + { + caption: '\\bibitem[]{}', + snippet: '\\bibitem[$1]{$2}', + meta: 'har2nat-cmd', + score: 0.3689547570562042 + }, + { + caption: '\\citet{}', + snippet: '\\citet{$1}', + meta: 'har2nat-cmd', + score: 0.09046048561361801 + }, + { + caption: '\\defcitealias{}{}', + snippet: '\\defcitealias{$1}{$2}', + meta: 'har2nat-cmd', + score: 0.00042021825647418025 + }, + { + caption: '\\aftergroup', + snippet: '\\aftergroup', + meta: 'har2nat-cmd', + score: 0.002020423627422133 + }, + { + caption: '\\setcitestyle{}', + snippet: '\\setcitestyle{$1}', + meta: 'har2nat-cmd', + score: 0.0015840652870152204 + }, + { + caption: '\\citeyearpar{}', + snippet: '\\citeyearpar{$1}', + meta: 'har2nat-cmd', + score: 0.001877888310324327 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'har2nat-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'har2nat-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\newblock', + snippet: '\\newblock', + meta: 'har2nat-cmd', + score: 0.03684301726876973 + }, + { + caption: '\\newblock{}', + snippet: '\\newblock{$1}', + meta: 'har2nat-cmd', + score: 0.03684301726876973 + }, + { + caption: '\\bibnumfmt', + snippet: '\\bibnumfmt', + meta: 'har2nat-cmd', + score: 0.000353353600267394 + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'har2nat-cmd', + score: 0.01091041305836494 + }, + { + caption: '\\citeauthor{}', + snippet: '\\citeauthor{$1}', + meta: 'har2nat-cmd', + score: 0.01359248786373484 + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'har2nat-cmd', + score: 0.03789745970461662 + } + ], + 'matlab-prettifier': [ + { + caption: '\\mlttfamily', + snippet: '\\mlttfamily', + meta: 'matlab-prettifier-cmd', + score: 0.000856282742498241 + }, + { + caption: '\\vskip', + snippet: '\\vskip', + meta: 'matlab-prettifier-cmd', + score: 0.05143052892347224 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'matlab-prettifier-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\thelstlisting', + snippet: '\\thelstlisting', + meta: 'matlab-prettifier-cmd', + score: 0.00012774128088872144 + }, + { + caption: '\\lstinputlisting[]{}', + snippet: '\\lstinputlisting[$1]{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.011660477607086044 + }, + { + caption: '\\lstinputlisting{}', + snippet: '\\lstinputlisting{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.011660477607086044 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'matlab-prettifier-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'matlab-prettifier-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\lstinline', + snippet: '\\lstinline', + meta: 'matlab-prettifier-cmd', + score: 0.005972262850694285 + }, + { + caption: '\\lstinline{}', + snippet: '\\lstinline{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.005972262850694285 + }, + { + caption: '\\lstlistoflistings', + snippet: '\\lstlistoflistings', + meta: 'matlab-prettifier-cmd', + score: 0.005279080363360602 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'matlab-prettifier-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'matlab-prettifier-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'matlab-prettifier-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'matlab-prettifier-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'matlab-prettifier-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.2864294797053033 + } + ], + datetime2: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'datetime2-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'datetime2-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'datetime2-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'datetime2-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datetime2-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'datetime2-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime2-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'datetime2-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'datetime2-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime2-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'datetime2-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'datetime2-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'datetime2-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime2-cmd', + score: 0.008565354665444157 + } + ], + lapdf: [ + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'lapdf-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lapdf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lapdf-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'lapdf-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'lapdf-cmd', + score: 0.028955796305270766 + } + ], + nccbbb: [ + { + caption: '\\bbbe', + snippet: '\\bbbe', + meta: 'nccbbb-cmd', + score: 0.0013332214754983353 + }, + { + caption: '\\bbbe[]', + snippet: '\\bbbe[$1]', + meta: 'nccbbb-cmd', + score: 0.0013332214754983353 + }, + { + caption: '\\bbbr', + snippet: '\\bbbr', + meta: 'nccbbb-cmd', + score: 0.0015739010274051707 + } + ], + tgbonum: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgbonum-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgbonum-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgbonum-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgbonum-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgbonum-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgbonum-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgbonum-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgbonum-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgbonum-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgbonum-cmd', + score: 0.021170869458413965 + } + ], + 'thm-restate': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-restate-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\listtheoremname', + snippet: '\\listtheoremname', + meta: 'thm-restate-cmd', + score: 1.9443373798666845e-5 + }, + { + caption: '\\thmtformatoptarg', + snippet: '\\thmtformatoptarg', + meta: 'thm-restate-cmd', + score: 6.353668036093916e-5 + }, + { + caption: '\\listoftheorems[]', + snippet: '\\listoftheorems[$1]', + meta: 'thm-restate-cmd', + score: 1.9443373798666845e-5 + }, + { + caption: '\\declaretheoremstyle[]{}', + snippet: '\\declaretheoremstyle[$1]{$2}', + meta: 'thm-restate-cmd', + score: 0.0001168034231635369 + }, + { + caption: '\\declaretheorem[]{}', + snippet: '\\declaretheorem[$1]{$2}', + meta: 'thm-restate-cmd', + score: 0.0004904790216915127 + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'thm-restate-cmd', + score: 0.02533412165007986 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thm-restate-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thm-restate-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thm-restate-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-restate-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-restate-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-restate-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-restate-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-restate-cmd', + score: 0.215689795055434 + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-restate-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-restate-cmd', + score: 0.0006133100544751855 + } + ], + 'biblatex-chicago': [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'biblatex-chicago-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'biblatex-chicago-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-chicago-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'biblatex-chicago-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-chicago-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'biblatex-chicago-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-chicago-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-chicago-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'biblatex-chicago-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-chicago-cmd', + score: 0.008565354665444157 + } + ], + pseudocode: [ + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'pseudocode-cmd', + score: 0.00107667147399019 + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'pseudocode-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'pseudocode-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'pseudocode-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'pseudocode-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pseudocode-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pseudocode-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pseudocode-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pseudocode-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pseudocode-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pseudocode-cmd', + score: 0.0018957469739775527 + } + ], + imakeidx: [ + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'imakeidx-cmd', + score: 0.010304996748556729 + }, + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'imakeidx-cmd', + score: 0.004417016910870522 + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'imakeidx-cmd', + score: 0.013774721817648336 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'imakeidx-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'imakeidx-cmd', + score: 0.008565354665444157 + } + ], + uri: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'uri-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'uri-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'uri-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'uri-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'uri-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'uri-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'uri-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'uri-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'uri-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'uri-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'uri-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'uri-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'uri-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'uri-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'uri-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'uri-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'uri-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'uri-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'uri-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'uri-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'uri-cmd', + score: 0.021170869458413965 + } + ], + tocvsec2: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tocvsec2-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tocvsec2-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tocvsec2-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tocvsec2-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tocvsec2-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tocvsec2-cmd', + score: 0.0018957469739775527 + } + ], + graphbox: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'graphbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'graphbox-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'graphbox-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'graphbox-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'graphbox-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'graphbox-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'graphbox-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'graphbox-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphbox-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'graphbox-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'graphbox-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphbox-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphbox-cmd', + score: 0.008565354665444157 + } + ], + limap: [ + { + caption: '\\MapContinuing{}', + snippet: '\\MapContinuing{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\MapTextFraction{}', + snippet: '\\MapTextFraction{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\MapBlockLabelFont{}', + snippet: '\\MapBlockLabelFont{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\Block{}', + snippet: '\\Block{$1}', + meta: 'limap-cmd', + score: 0.011618215341095648 + }, + { + caption: '\\MapRuleWidth{}', + snippet: '\\MapRuleWidth{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\MapTitleFraction{}', + snippet: '\\MapTitleFraction{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\MapContinued{}', + snippet: '\\MapContinued{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\WideBlock{}', + snippet: '\\WideBlock{$1}', + meta: 'limap-cmd', + score: 0.002453536158989143 + }, + { + caption: '\\MapParskip{}', + snippet: '\\MapParskip{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'limap-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'limap-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'limap-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'limap-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'limap-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'limap-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'limap-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'limap-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'limap-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'limap-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'limap-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'limap-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'limap-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'limap-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 0.004974385202605165 + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'limap-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'limap-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'limap-cmd', + score: 0.04533364657852219 + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'limap-cmd', + score: 0.07098077735912875 + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'limap-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'limap-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'limap-cmd', + score: 0.059857788139528495 + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'limap-cmd', + score: 0.0023853501147448834 + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'limap-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'limap-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'limap-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'limap-cmd', + score: 9.952664522415981e-5 + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'limap-cmd', + score: 0.0016148498709822416 + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'limap-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'limap-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'limap-cmd', + score: 0.0029238994233674776 + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'limap-cmd', + score: 0.0313525090421608 + } + ], + tikzscale: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzscale-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzscale-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikzscale-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikzscale-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikzscale-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzscale-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikzscale-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikzscale-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tikzscale-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tikzscale-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tikzscale-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tikzscale-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzscale-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tikzscale-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tikzscale-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzscale-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzscale-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tikzscale-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tikzscale-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikzscale-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikzscale-cmd', + score: 0.2864294797053033 + } + ], + savesym: [ + { + caption: '\\savesymbol{}', + snippet: '\\savesymbol{$1}', + meta: 'savesym-cmd', + score: 6.662041157021826e-5 + } + ], + subscript: [ + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'subscript-cmd', + score: 0.058405875394131175 + } + ], + letterspace: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'letterspace-cmd', + score: 0.00037306820619479756 + } + ], + mathastext: [ + { + caption: '\\Huge', + snippet: '\\Huge', + meta: 'mathastext-cmd', + score: 0.04725806985998919 + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'mathastext-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'mathastext-cmd', + score: 0.008427383388519996 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'mathastext-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\mathrm{}', + snippet: '\\mathrm{$1}', + meta: 'mathastext-cmd', + score: 0.19117752976172653 + } + ], + movie15: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'movie15-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'movie15-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'movie15-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'movie15-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'movie15-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'movie15-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'movie15-cmd', + score: 0.0018957469739775527 + } + ], + refstyle: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'refstyle-cmd', + score: 0.00037306820619479756 + } + ], + 'pst-3d': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-3d-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-3d-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-3d-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-3d-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-3d-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-3d-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-3d-cmd', + score: 0.006520475264573554 + } + ], + rotfloat: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0009837365348002915 + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0005815474978918903 + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'rotfloat-cmd', + score: 0.0008866338267686714 + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'rotfloat-cmd', + score: 0.0015470917047414941 + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0011934321931750752 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotfloat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'rotfloat-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'rotfloat-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'rotfloat-cmd', + score: 0.0012745874472536625 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotfloat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'rotfloat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'rotfloat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'rotfloat-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'rotfloat-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'rotfloat-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rotfloat-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'rotfloat-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotfloat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'rotfloat-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'rotfloat-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'rotfloat-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'rotfloat-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'rotfloat-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'rotfloat-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707 + } + ], + progressbar: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'progressbar-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'progressbar-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'progressbar-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'progressbar-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'progressbar-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'progressbar-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'progressbar-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'progressbar-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'progressbar-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'progressbar-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'progressbar-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'progressbar-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'progressbar-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'progressbar-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'progressbar-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'progressbar-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'progressbar-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'progressbar-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'progressbar-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157 + } + ], + pagecolor: [ + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pagecolor-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pagecolor-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pagecolor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pagecolor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pagecolor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pagecolor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pagecolor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pagecolor-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pagecolor-cmd', + score: 0.021170869458413965 + } + ], + gb4e: [ + { + caption: '\\ex', + snippet: '\\ex', + meta: 'gb4e-cmd', + score: 0.00916111174873264 + } + ], + ESIEEcv: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'ESIEEcv-cmd', + score: 0.03789745970461662 + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ESIEEcv-cmd', + score: 0.0008038857295393196 + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ESIEEcv-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ESIEEcv-cmd', + score: 0.00048507499766588637 + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ESIEEcv-cmd', + score: 0.0005861357565780464 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ESIEEcv-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ESIEEcv-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ESIEEcv-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ESIEEcv-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ESIEEcv-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ESIEEcv-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ESIEEcv-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ESIEEcv-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ESIEEcv-cmd', + score: 0.018615449342361392 + } + ], + ftnright: [ + { + caption: '\\footnotesize', + snippet: '\\footnotesize', + meta: 'ftnright-cmd', + score: 0.2038592081252624 + }, + { + caption: '\\footnotesize{}', + snippet: '\\footnotesize{$1}', + meta: 'ftnright-cmd', + score: 0.2038592081252624 + } + ], + chemformula: [ + { + caption: '\\ch{}', + snippet: '\\ch{$1}', + meta: 'chemformula-cmd', + score: 0.0013276105116845872 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'chemformula-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'chemformula-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\nicefrac{}{}', + snippet: '\\nicefrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0018011350423659288 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'chemformula-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'chemformula-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'chemformula-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'chemformula-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'chemformula-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'chemformula-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'chemformula-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'chemformula-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'chemformula-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'chemformula-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'chemformula-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'chemformula-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'chemformula-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'chemformula-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'chemformula-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'chemformula-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'chemformula-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'chemformula-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'chemformula-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'chemformula-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'chemformula-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'chemformula-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'chemformula-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'chemformula-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'chemformula-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'chemformula-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'chemformula-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'chemformula-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'chemformula-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'chemformula-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'chemformula-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'chemformula-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'chemformula-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'chemformula-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'chemformula-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'chemformula-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'chemformula-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'chemformula-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'chemformula-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'chemformula-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'chemformula-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'chemformula-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'chemformula-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'chemformula-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'chemformula-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'chemformula-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'chemformula-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'chemformula-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'chemformula-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'chemformula-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'chemformula-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'chemformula-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'chemformula-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chemformula-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemformula-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemformula-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chemformula-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chemformula-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chemformula-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemformula-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chemformula-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chemformula-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemformula-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chemformula-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chemformula-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemformula-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemformula-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chemformula-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemformula-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chemformula-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chemformula-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'chemformula-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'chemformula-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'chemformula-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'chemformula-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'chemformula-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'chemformula-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'chemformula-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'chemformula-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'chemformula-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'chemformula-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'chemformula-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'chemformula-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'chemformula-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'chemformula-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'chemformula-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'chemformula-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'chemformula-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'chemformula-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'chemformula-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'chemformula-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'chemformula-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'chemformula-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'chemformula-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'chemformula-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'chemformula-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'chemformula-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'chemformula-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'chemformula-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'chemformula-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'chemformula-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'chemformula-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'chemformula-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'chemformula-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'chemformula-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'chemformula-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'chemformula-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'chemformula-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'chemformula-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'chemformula-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'chemformula-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'chemformula-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'chemformula-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'chemformula-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'chemformula-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'chemformula-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'chemformula-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'chemformula-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'chemformula-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'chemformula-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'chemformula-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'chemformula-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'chemformula-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'chemformula-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'chemformula-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'chemformula-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'chemformula-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'chemformula-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'chemformula-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'chemformula-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'chemformula-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'chemformula-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'chemformula-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'chemformula-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'chemformula-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'chemformula-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'chemformula-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'chemformula-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'chemformula-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'chemformula-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'chemformula-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'chemformula-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'chemformula-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'chemformula-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'chemformula-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'chemformula-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'chemformula-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'chemformula-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'chemformula-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'chemformula-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'chemformula-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'chemformula-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'chemformula-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'chemformula-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'chemformula-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'chemformula-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'chemformula-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemformula-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\sfrac{}{}', + snippet: '\\sfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0030164694688453453 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'chemformula-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'chemformula-cmd', + score: 0.0063276692758974925 + } + ], + pgfautomata: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfautomata-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfautomata-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfautomata-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfautomata-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfautomata-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfautomata-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfautomata-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfautomata-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfautomata-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfautomata-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfautomata-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfautomata-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfautomata-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfautomata-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfautomata-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfautomata-cmd', + score: 0.2864294797053033 + } + ], + pgfnodes: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfnodes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfnodes-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfnodes-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfnodes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfnodes-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfnodes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfnodes-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfnodes-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfnodes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfnodes-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfnodes-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfnodes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfnodes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfnodes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfnodes-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfnodes-cmd', + score: 0.2864294797053033 + } + ], + pgfarrows: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfarrows-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfarrows-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfarrows-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfarrows-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfarrows-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfarrows-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfarrows-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfarrows-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfarrows-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfarrows-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfarrows-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfarrows-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfarrows-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfarrows-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfarrows-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfarrows-cmd', + score: 0.2864294797053033 + } + ], + 'pst-text': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-text-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-text-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-text-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-text-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-text-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-text-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-text-cmd', + score: 0.006520475264573554 + } + ], + keystroke: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'keystroke-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'keystroke-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'keystroke-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'keystroke-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'keystroke-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'keystroke-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'keystroke-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'keystroke-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'keystroke-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'keystroke-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'keystroke-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'keystroke-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'keystroke-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'keystroke-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'keystroke-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'keystroke-cmd', + score: 0.004649150613625593 + } + ], + currvita: [ + { + caption: '\\cvheadingfont', + snippet: '\\cvheadingfont', + meta: 'currvita-cmd', + score: 5.547871753177405e-5 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'currvita-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'currvita-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'currvita-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'currvita-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'currvita-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'currvita-cmd', + score: 0.0018957469739775527 + } + ], + subfigmat: [ + { + caption: '\\subfigure[]{}', + snippet: '\\subfigure[$1]{$2}', + meta: 'subfigmat-cmd', + score: 0.037856842641104005 + }, + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subfigmat-cmd', + score: 0.007192033516871399 + }, + { + caption: '\\subfigure[]{}', + snippet: '\\subfigure[$1]{$2}', + meta: 'subfigmat-cmd', + score: 0.037856842641104005 + } + ], + boxhandler: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'boxhandler-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'boxhandler-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'boxhandler-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'boxhandler-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'boxhandler-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\pbox{}{}', + snippet: '\\pbox{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.0010883030320478486 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'boxhandler-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'boxhandler-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'boxhandler-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'boxhandler-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'boxhandler-cmd', + score: 0.028955796305270766 + } + ], + media9: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'media9-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'media9-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'media9-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'media9-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'media9-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'media9-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'media9-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'media9-cmd', + score: 0.2864294797053033 + } + ], + translator: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'translator-cmd', + score: 0.00037306820619479756 + } + ], + german: [ + { + caption: '\\today', + snippet: '\\today', + meta: 'german-cmd', + score: 0.10733849317324783 + } + ], + mhsetup: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mhsetup-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mhsetup-cmd', + score: 0.021170869458413965 + } + ], + nomentbl: [ + { + caption: '\\nomenclature[]{}{}', + snippet: '\\nomenclature[$1]{$2}{$3}', + meta: 'nomentbl-cmd', + score: 0.016053526743355948 + }, + { + caption: '\\nomenclature{}{}', + snippet: '\\nomenclature{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.016053526743355948 + }, + { + caption: '\\nomlabel', + snippet: '\\nomlabel', + meta: 'nomentbl-cmd', + score: 6.353668036093916e-5 + }, + { + caption: '\\printnomenclature', + snippet: '\\printnomenclature', + meta: 'nomentbl-cmd', + score: 0.0014526113324237952 + }, + { + caption: '\\printnomenclature[]', + snippet: '\\printnomenclature[$1]', + meta: 'nomentbl-cmd', + score: 0.0014526113324237952 + }, + { + caption: '\\makenomenclature', + snippet: '\\makenomenclature', + meta: 'nomentbl-cmd', + score: 0.002310610204652063 + }, + { + caption: '\\nomgroup', + snippet: '\\nomgroup', + meta: 'nomentbl-cmd', + score: 0.0005549290951493257 + }, + { + caption: '\\nomgroup[]{}', + snippet: '\\nomgroup[$1]{$2}', + meta: 'nomentbl-cmd', + score: 0.0005549290951493257 + }, + { + caption: '\\nomname', + snippet: '\\nomname', + meta: 'nomentbl-cmd', + score: 0.0015092617929470952 + }, + { + caption: '\\nompreamble', + snippet: '\\nompreamble', + meta: 'nomentbl-cmd', + score: 2.4350510995473236e-5 + }, + { + caption: '\\nomentryend', + snippet: '\\nomentryend', + meta: 'nomentbl-cmd', + score: 0.000137692304514793 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'nomentbl-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nomentbl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nomentbl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'nomentbl-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'nomentbl-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'nomentbl-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'nomentbl-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'nomentbl-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'nomentbl-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'nomentbl-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nomentbl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'nomentbl-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'nomentbl-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'nomentbl-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'nomentbl-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'nomentbl-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'nomentbl-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'nomentbl-cmd', + score: 0.0023853501147448834 + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'nomentbl-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nomentbl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nomentbl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'nomentbl-cmd', + score: 9.952664522415981e-5 + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'nomentbl-cmd', + score: 0.0016148498709822416 + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'nomentbl-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'nomentbl-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'nomentbl-cmd', + score: 0.0029238994233674776 + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'nomentbl-cmd', + score: 0.0313525090421608 + } + ], + miller: [ + { + caption: '\\hkl', + snippet: '\\hkl', + meta: 'miller-cmd', + score: 0.0034259481311452946 + }, + { + caption: '\\hkl{}', + snippet: '\\hkl{$1}', + meta: 'miller-cmd', + score: 0.0034259481311452946 + }, + { + caption: '\\hkl[]', + snippet: '\\hkl[$1]', + meta: 'miller-cmd', + score: 0.0034259481311452946 + } + ], + lpform: [ + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'lpform-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'lpform-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'lpform-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lpform-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lpform-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'lpform-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'lpform-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'lpform-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'lpform-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'lpform-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'lpform-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'lpform-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'lpform-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'lpform-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'lpform-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'lpform-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'lpform-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'lpform-cmd', + score: 0.009331077109224957 + } + ], + xepersian: [ + { + caption: '\\settextfont[]{}', + snippet: '\\settextfont[$1]{$2}', + meta: 'xepersian-cmd', + score: 0.00015447355412753335 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xepersian-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xepersian-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'xepersian-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'xepersian-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xepersian-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xepersian-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xepersian-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'xepersian-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'xepersian-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'xepersian-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xepersian-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xepersian-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xepersian-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927 + } + ], + chapterbib: [ + { + caption: '\\bibliographystyle{}', + snippet: '\\bibliographystyle{$1}', + meta: 'chapterbib-cmd', + score: 0.25122317941387773 + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'chapterbib-cmd', + score: 0.2659628337907604 + }, + { + caption: '\\include{}', + snippet: '\\include{$1}', + meta: 'chapterbib-cmd', + score: 0.1547080054979312 + } + ], + scalerel: [ + { + caption: '\\scaleto{}{}', + snippet: '\\scaleto{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00027615383978106523 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scalerel-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'scalerel-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'scalerel-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'scalerel-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'scalerel-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'scalerel-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'scalerel-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'scalerel-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'scalerel-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'scalerel-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'scalerel-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'scalerel-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'scalerel-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scalerel-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'scalerel-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'scalerel-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'scalerel-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'scalerel-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'scalerel-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'scalerel-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'scalerel-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'scalerel-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'scalerel-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'scalerel-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'scalerel-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'scalerel-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'scalerel-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scalerel-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'scalerel-cmd', + score: 0.004719094298848707 + } + ], + extarrows: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'extarrows-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'extarrows-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'extarrows-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'extarrows-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'extarrows-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'extarrows-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'extarrows-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'extarrows-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'extarrows-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'extarrows-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'extarrows-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'extarrows-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'extarrows-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'extarrows-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'extarrows-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'extarrows-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'extarrows-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'extarrows-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'extarrows-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'extarrows-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'extarrows-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'extarrows-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'extarrows-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'extarrows-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'extarrows-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'extarrows-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'extarrows-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'extarrows-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'extarrows-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'extarrows-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'extarrows-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'extarrows-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'extarrows-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'extarrows-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'extarrows-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'extarrows-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'extarrows-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'extarrows-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'extarrows-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'extarrows-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'extarrows-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'extarrows-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'extarrows-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'extarrows-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'extarrows-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'extarrows-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'extarrows-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'extarrows-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'extarrows-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'extarrows-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'extarrows-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'extarrows-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'extarrows-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'extarrows-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'extarrows-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'extarrows-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'extarrows-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'extarrows-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'extarrows-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'extarrows-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'extarrows-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'extarrows-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'extarrows-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'extarrows-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'extarrows-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'extarrows-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'extarrows-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'extarrows-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'extarrows-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'extarrows-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'extarrows-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'extarrows-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'extarrows-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'extarrows-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'extarrows-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'extarrows-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'extarrows-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'extarrows-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'extarrows-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'extarrows-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'extarrows-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'extarrows-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'extarrows-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'extarrows-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'extarrows-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'extarrows-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'extarrows-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'extarrows-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'extarrows-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'extarrows-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'extarrows-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'extarrows-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'extarrows-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'extarrows-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'extarrows-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'extarrows-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'extarrows-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'extarrows-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'extarrows-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'extarrows-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'extarrows-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'extarrows-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'extarrows-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'extarrows-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'extarrows-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'extarrows-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'extarrows-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'extarrows-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'extarrows-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'extarrows-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'extarrows-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'extarrows-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'extarrows-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'extarrows-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'extarrows-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'extarrows-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'extarrows-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'extarrows-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'extarrows-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'extarrows-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'extarrows-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'extarrows-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'extarrows-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'extarrows-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'extarrows-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'extarrows-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'extarrows-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'extarrows-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'extarrows-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'extarrows-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'extarrows-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'extarrows-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'extarrows-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'extarrows-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'extarrows-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'extarrows-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'extarrows-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'extarrows-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'extarrows-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'extarrows-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'extarrows-cmd', + score: 0.0063276692758974925 + } + ], + listingsutf8: [ + { + caption: '\\vskip', + snippet: '\\vskip', + meta: 'listingsutf8-cmd', + score: 0.05143052892347224 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'listingsutf8-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'listingsutf8-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'listingsutf8-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\thelstlisting', + snippet: '\\thelstlisting', + meta: 'listingsutf8-cmd', + score: 0.00012774128088872144 + }, + { + caption: '\\lstinputlisting[]{}', + snippet: '\\lstinputlisting[$1]{$2}', + meta: 'listingsutf8-cmd', + score: 0.011660477607086044 + }, + { + caption: '\\lstinputlisting{}', + snippet: '\\lstinputlisting{$1}', + meta: 'listingsutf8-cmd', + score: 0.011660477607086044 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'listingsutf8-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'listingsutf8-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\lstinline', + snippet: '\\lstinline', + meta: 'listingsutf8-cmd', + score: 0.005972262850694285 + }, + { + caption: '\\lstinline{}', + snippet: '\\lstinline{$1}', + meta: 'listingsutf8-cmd', + score: 0.005972262850694285 + }, + { + caption: '\\lstlistoflistings', + snippet: '\\lstlistoflistings', + meta: 'listingsutf8-cmd', + score: 0.005279080363360602 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'listingsutf8-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'listingsutf8-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'listingsutf8-cmd', + score: 0.00037306820619479756 + } + ], + forloop: [ + { + caption: '\\forloop{}{}{}{}', + snippet: '\\forloop{$1}{$2}{$3}{$4}', + meta: 'forloop-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'forloop-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'forloop-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'forloop-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'forloop-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'forloop-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'forloop-cmd', + score: 0.0018957469739775527 + } + ], + xymtex: [ + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'xymtex-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'xymtex-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'xymtex-cmd', + score: 0.35084018920966636 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'xymtex-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xymtex-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xymtex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xymtex-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xymtex-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xymtex-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xymtex-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xymtex-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xymtex-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xymtex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xymtex-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xymtex-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xymtex-cmd', + score: 0.2864294797053033 + } + ], + eqlist: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'eqlist-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'eqlist-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'eqlist-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'eqlist-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'eqlist-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqlist-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'eqlist-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'eqlist-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqlist-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'eqlist-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'eqlist-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\eqparbox{}{}', + snippet: '\\eqparbox{$1}{$2}', + meta: 'eqlist-cmd', + score: 2.9423534119530166e-5 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'eqlist-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'eqlist-cmd', + score: 3.800886892251021 + } + ], + tgschola: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgschola-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgschola-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgschola-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgschola-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgschola-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgschola-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgschola-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgschola-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgschola-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgschola-cmd', + score: 0.021170869458413965 + } + ], + mfirstuc: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mfirstuc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mfirstuc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'mfirstuc-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mfirstuc-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mfirstuc-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'mfirstuc-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'mfirstuc-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'mfirstuc-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'mfirstuc-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mfirstuc-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'mfirstuc-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mfirstuc-cmd', + score: 0.008565354665444157 + } + ], + gloss: [ + { + caption: '\\makegloss', + snippet: '\\makegloss', + meta: 'gloss-cmd', + score: 0.0018653410309739879 + } + ], + ltxcmds: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ltxcmds-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'ltxcmds-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ltxcmds-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ltxcmds-cmd', + score: 0.021170869458413965 + } + ], + outlines: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'outlines-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'outlines-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'outlines-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'outlines-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'outlines-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'outlines-cmd', + score: 0.0018957469739775527 + } + ], + typearea: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'typearea-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'typearea-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'typearea-cmd', + score: 0.0008555564394100388 + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'typearea-cmd', + score: 0.012985816912639263 + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'typearea-cmd', + score: 0.000396664302361659 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'typearea-cmd', + score: 0.00037306820619479756 + } + ], + currfile: [ + { + caption: '\\currfiledir', + snippet: '\\currfiledir', + meta: 'currfile-cmd', + score: 0.0002459788020229296 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'currfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'currfile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'currfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'currfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'currfile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'currfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'currfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'currfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'currfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'currfile-cmd', + score: 0.021170869458413965 + } + ], + toptesi: [ + { + caption: '\\tomo', + snippet: '\\tomo', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\mainmatter', + snippet: '\\mainmatter', + meta: 'toptesi-cmd', + score: 0.025705092792367497 + }, + { + caption: '\\ringraziamenti', + snippet: '\\ringraziamenti', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\sommario', + snippet: '\\sommario', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\NoteWhiteLine', + snippet: '\\NoteWhiteLine', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\paginavuota', + snippet: '\\paginavuota', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\nota{}', + snippet: '\\nota{$1}', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\indici', + snippet: '\\indici', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'toptesi-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'toptesi-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'toptesi-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'toptesi-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'toptesi-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'toptesi-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'toptesi-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'toptesi-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'toptesi-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'toptesi-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'toptesi-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'toptesi-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'toptesi-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'toptesi-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'toptesi-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'toptesi-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'toptesi-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'toptesi-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'toptesi-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'toptesi-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'toptesi-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'toptesi-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'toptesi-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'toptesi-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\listing{}', + snippet: '\\listing{$1}', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\micro', + snippet: '\\micro', + meta: 'toptesi-cmd', + score: 0.011051971930487929 + }, + { + caption: '\\gradi', + snippet: '\\gradi', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + }, + { + caption: '\\unit[]{}', + snippet: '\\unit[$1]{$2}', + meta: 'toptesi-cmd', + score: 0.028299796173135428 + }, + { + caption: '\\unit{}', + snippet: '\\unit{$1}', + meta: 'toptesi-cmd', + score: 0.028299796173135428 + }, + { + caption: '\\ped{}', + snippet: '\\ped{$1}', + meta: 'toptesi-cmd', + score: 0.0007129548652040002 + }, + { + caption: '\\ohm', + snippet: '\\ohm', + meta: 'toptesi-cmd', + score: 0.0038146685721293138 + }, + { + caption: '\\gei', + snippet: '\\gei', + meta: 'toptesi-cmd', + score: 0.00023765162173466673 + } + ], + amsrefs: [ + { + caption: '\\ndash', + snippet: '\\ndash', + meta: 'amsrefs-cmd', + score: 0.0003420867634658178 + }, + { + caption: '\\bib{}{}{}', + snippet: '\\bib{$1}{$2}{$3}', + meta: 'amsrefs-cmd', + score: 0.0017473230242849183 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'amsrefs-cmd', + score: 2.341195220791228 + } + ], + sistyle: [ + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'sistyle-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'sistyle-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'sistyle-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'sistyle-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'sistyle-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'sistyle-cmd', + score: 0.0063276692758974925 + } + ], + suffix: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'suffix-cmd', + score: 0.03789745970461662 + } + ], + sansmath: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'sansmath-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'sansmath-cmd', + score: 0.021170869458413965 + } + ], + 'tikz-qtree': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-qtree-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-qtree-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-qtree-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-qtree-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-qtree-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-qtree-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-qtree-cmd', + score: 0.2864294797053033 + } + ], + floatpag: [ + { + caption: '\\rotfloatpagestyle{}', + snippet: '\\rotfloatpagestyle{$1}', + meta: 'floatpag-cmd', + score: 0.0004535003423927585 + }, + { + caption: '\\floatpagestyle{}', + snippet: '\\floatpagestyle{$1}', + meta: 'floatpag-cmd', + score: 0.0004535003423927585 + } + ], + colortab: [ + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'colortab-cmd', + score: 0.00107667147399019 + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'colortab-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'colortab-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'colortab-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'colortab-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'colortab-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'colortab-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'colortab-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'colortab-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'colortab-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'colortab-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'colortab-cmd', + score: 0.006520475264573554 + } + ], + parcolumns: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'parcolumns-cmd', + score: 0.00037306820619479756 + } + ], + dingbat: [ + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'dingbat-cmd', + score: 0.025060530944368123 + } + ], + ifoddpage: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ifoddpage-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\checkoddpage', + snippet: '\\checkoddpage', + meta: 'ifoddpage-cmd', + score: 0.00028672585452906425 + } + ], + kvoptions: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvoptions-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'kvoptions-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvoptions-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvoptions-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'kvoptions-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvoptions-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvoptions-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvoptions-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'kvoptions-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'kvoptions-cmd', + score: 0.021170869458413965 + } + ], + 'pst-tree': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-tree-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-tree-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-tree-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-tree-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-tree-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-tree-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-tree-cmd', + score: 0.006520475264573554 + } + ], + nonfloat: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'nonfloat-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'nonfloat-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'nonfloat-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'nonfloat-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'nonfloat-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'nonfloat-cmd', + score: 0.0018957469739775527 + } + ], + rsphrase: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'rsphrase-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'rsphrase-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'rsphrase-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'rsphrase-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'rsphrase-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'rsphrase-cmd', + score: 0.0018957469739775527 + } + ], + beramono: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'beramono-cmd', + score: 0.00037306820619479756 + } + ], + pgfbaseimage: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfbaseimage-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfbaseimage-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfbaseimage-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfbaseimage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfbaseimage-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfbaseimage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfbaseimage-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfbaseimage-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.2864294797053033 + } + ], + romannum: [ + { + caption: '\\thefootnote', + snippet: '\\thefootnote', + meta: 'romannum-cmd', + score: 0.007676927812687567 + }, + { + caption: '\\thefootnote{}', + snippet: '\\thefootnote{$1}', + meta: 'romannum-cmd', + score: 0.007676927812687567 + } + ], + tgtermes: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgtermes-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgtermes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgtermes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgtermes-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgtermes-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgtermes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgtermes-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgtermes-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgtermes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgtermes-cmd', + score: 0.021170869458413965 + } + ], + Alegreya: [ + { + caption: '\\rmfamily', + snippet: '\\rmfamily', + meta: 'Alegreya-cmd', + score: 0.00898937903263608 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'Alegreya-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'Alegreya-cmd', + score: 0.008565354665444157 + } + ], + 'glossaries-extra': [ + { + caption: '\\gls{}', + snippet: '\\gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.06939353309055077 + }, + { + caption: '\\Gls{}', + snippet: '\\Gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.003696678698317109 + }, + { + caption: '\\makeglossaries', + snippet: '\\makeglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0056737600836936995 + }, + { + caption: '\\newabbreviation{}{}{}', + snippet: '\\newabbreviation{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.00023275591440052114 + }, + { + caption: '\\newglossaryentry{}{}', + snippet: '\\newglossaryentry{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.018524394136900962 + }, + { + caption: '\\newglossary{}{}', + snippet: '\\newglossary{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 1.4547244650032571e-5 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\glslongpluralkey', + snippet: '\\glslongpluralkey', + meta: 'glossaries-extra-cmd', + score: 1.4538687447297259e-5 + }, + { + caption: '\\Glspl{}', + snippet: '\\Glspl{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0025291265119320736 + }, + { + caption: '\\glossarysection', + snippet: '\\glossarysection', + meta: 'glossaries-extra-cmd', + score: 9.579755294730752e-5 + }, + { + caption: '\\printglossaries', + snippet: '\\printglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0010106582768889887 + }, + { + caption: '\\Gls{}', + snippet: '\\Gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.003696678698317109 + }, + { + caption: '\\setglossarystyle{}', + snippet: '\\setglossarystyle{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0003758893277679221 + }, + { + caption: '\\printglossary', + snippet: '\\printglossary', + meta: 'glossaries-extra-cmd', + score: 0.009139682306158714 + }, + { + caption: '\\printglossary[]', + snippet: '\\printglossary[$1]', + meta: 'glossaries-extra-cmd', + score: 0.009139682306158714 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\setglossarysection{}', + snippet: '\\setglossarysection{$1}', + meta: 'glossaries-extra-cmd', + score: 3.6081414102781514e-5 + }, + { + caption: '\\glsresetall', + snippet: '\\glsresetall', + meta: 'glossaries-extra-cmd', + score: 0.0006123462672467326 + }, + { + caption: '\\the', + snippet: '\\the', + meta: 'glossaries-extra-cmd', + score: 0.007238960303946444 + }, + { + caption: '\\acrshort{}', + snippet: '\\acrshort{$1}', + meta: 'glossaries-extra-cmd', + score: 0.009936841864059727 + }, + { + caption: '\\printnoidxglossary[]', + snippet: '\\printnoidxglossary[$1]', + meta: 'glossaries-extra-cmd', + score: 0.00021912375285685037 + }, + { + caption: '\\newglossary{}{}', + snippet: '\\newglossary{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 1.4547244650032571e-5 + }, + { + caption: '\\gls{}', + snippet: '\\gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.06939353309055077 + }, + { + caption: '\\printnoidxglossaries', + snippet: '\\printnoidxglossaries', + meta: 'glossaries-extra-cmd', + score: 5.6789564226023136e-5 + }, + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'glossaries-extra-cmd', + score: 0.004417016910870522 + }, + { + caption: '\\defglsentryfmt[]{}', + snippet: '\\defglsentryfmt[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\glspostdescription', + snippet: '\\glspostdescription', + meta: 'glossaries-extra-cmd', + score: 0.0006337376579591112 + }, + { + caption: '\\number', + snippet: '\\number', + meta: 'glossaries-extra-cmd', + score: 0.000968714260809983 + }, + { + caption: '\\glsaddall', + snippet: '\\glsaddall', + meta: 'glossaries-extra-cmd', + score: 0.0008363820557740373 + }, + { + caption: '\\glsaddall[]', + snippet: '\\glsaddall[$1]', + meta: 'glossaries-extra-cmd', + score: 0.0008363820557740373 + }, + { + caption: '\\makeglossaries', + snippet: '\\makeglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0056737600836936995 + }, + { + caption: '\\glossaryname', + snippet: '\\glossaryname', + meta: 'glossaries-extra-cmd', + score: 0.0006174536302752427 + }, + { + caption: '\\newglossaryentry{}{}', + snippet: '\\newglossaryentry{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.018524394136900962 + }, + { + caption: '\\glslabel', + snippet: '\\glslabel', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\glsadd{}', + snippet: '\\glsadd{$1}', + meta: 'glossaries-extra-cmd', + score: 3.0150373480213892e-5 + }, + { + caption: '\\makenoidxglossaries', + snippet: '\\makenoidxglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0001382210125680805 + }, + { + caption: '\\glsgenentryfmt', + snippet: '\\glsgenentryfmt', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\acronymtype', + snippet: '\\acronymtype', + meta: 'glossaries-extra-cmd', + score: 0.002000834271117562 + }, + { + caption: '\\acrfull{}', + snippet: '\\acrfull{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0032622587277765067 + }, + { + caption: '\\newacronym{}{}{}', + snippet: '\\newacronym{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.03193935544723102 + }, + { + caption: '\\glspl{}', + snippet: '\\glspl{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0034025897522047717 + }, + { + caption: '\\ifglsused{}{}{}', + snippet: '\\ifglsused{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5 + }, + { + caption: '\\acrlong{}', + snippet: '\\acrlong{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002517821598213752 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'glossaries-extra-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'glossaries-extra-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'glossaries-extra-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'glossaries-extra-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'glossaries-extra-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'glossaries-extra-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'glossaries-extra-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'glossaries-extra-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'glossaries-extra-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'glossaries-extra-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'glossaries-extra-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'glossaries-extra-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'glossaries-extra-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'glossaries-extra-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'glossaries-extra-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'glossaries-extra-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'glossaries-extra-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'glossaries-extra-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'glossaries-extra-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'glossaries-extra-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'glossaries-extra-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'glossaries-extra-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'glossaries-extra-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'glossaries-extra-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'glossaries-extra-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'glossaries-extra-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'glossaries-extra-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'glossaries-extra-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'glossaries-extra-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'glossaries-extra-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'glossaries-extra-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'glossaries-extra-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'glossaries-extra-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'glossaries-extra-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'glossaries-extra-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'glossaries-extra-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'glossaries-extra-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'glossaries-extra-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'glossaries-extra-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'glossaries-extra-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'glossaries-extra-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'glossaries-extra-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'glossaries-extra-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'glossaries-extra-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'glossaries-extra-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'glossaries-extra-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'glossaries-extra-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'glossaries-extra-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'glossaries-extra-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'glossaries-extra-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'glossaries-extra-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'glossaries-extra-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'glossaries-extra-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'glossaries-extra-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'glossaries-extra-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'glossaries-extra-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'glossaries-extra-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'glossaries-extra-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'glossaries-extra-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'glossaries-extra-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'glossaries-extra-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'glossaries-extra-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'glossaries-extra-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'glossaries-extra-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'glossaries-extra-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'glossaries-extra-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'glossaries-extra-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'glossaries-extra-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'glossaries-extra-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'glossaries-extra-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'glossaries-extra-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'glossaries-extra-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'glossaries-extra-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'glossaries-extra-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'glossaries-extra-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'glossaries-extra-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'glossaries-extra-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'glossaries-extra-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'glossaries-extra-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'glossaries-extra-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'glossaries-extra-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'glossaries-extra-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'glossaries-extra-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'glossaries-extra-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'glossaries-extra-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'glossaries-extra-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'glossaries-extra-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'glossaries-extra-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'glossaries-extra-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'glossaries-extra-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'glossaries-extra-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'glossaries-extra-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'glossaries-extra-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'glossaries-extra-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'glossaries-extra-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'glossaries-extra-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'glossaries-extra-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'glossaries-extra-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'glossaries-extra-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'glossaries-extra-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'glossaries-extra-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'glossaries-extra-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'glossaries-extra-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'glossaries-extra-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'glossaries-extra-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'glossaries-extra-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-extra-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'glossaries-extra-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'glossaries-extra-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'glossaries-extra-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'glossaries-extra-cmd', + score: 2.341195220791228 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'glossaries-extra-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'glossaries-extra-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'glossaries-extra-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'glossaries-extra-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-extra-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'glossaries-extra-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-extra-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'glossaries-extra-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'glossaries-extra-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-extra-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'glossaries-extra-cmd', + score: 0.0063276692758974925 + } + ], + dashrule: [ + { + caption: '\\hdashrule[]{}{}{}', + snippet: '\\hdashrule[$1]{$2}{$3}{$4}', + meta: 'dashrule-cmd', + score: 0.00029867998381154486 + } + ], + bclogo: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bclogo-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'bclogo-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'bclogo-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bclogo-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'bclogo-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'bclogo-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'bclogo-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'bclogo-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'bclogo-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bclogo-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'bclogo-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'bclogo-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bclogo-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bclogo-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bclogo-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'bclogo-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'bclogo-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'bclogo-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bclogo-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'bclogo-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bclogo-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'bclogo-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bclogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'bclogo-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'bclogo-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'bclogo-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'bclogo-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bclogo-cmd', + score: 0.004719094298848707 + } + ], + isomath: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'isomath-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'isomath-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'isomath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'isomath-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'isomath-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'isomath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'isomath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'isomath-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'isomath-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'isomath-cmd', + score: 0.021170869458413965 + } + ], + 'tkz-graph': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-graph-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-graph-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-graph-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-graph-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-graph-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-graph-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-graph-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-graph-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tkz-graph-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tkz-graph-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tkz-graph-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tkz-graph-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-graph-cmd', + score: 0.0018653410309739879 + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-graph-cmd', + score: 0.00031058155311734754 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-graph-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-graph-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-graph-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-graph-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-graph-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-graph-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-graph-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-graph-cmd', + score: 0.2864294797053033 + } + ], + sourcesanspro: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'sourcesanspro-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'sourcesanspro-cmd', + score: 0.008565354665444157 + } + ], + longdivision: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'longdivision-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'longdivision-cmd', + score: 0.2864294797053033 + } + ], + xmpmulti: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xmpmulti-cmd', + score: 0.00037306820619479756 + } + ], + epsdice: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsdice-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'epsdice-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'epsdice-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'epsdice-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'epsdice-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'epsdice-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epsdice-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'epsdice-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsdice-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'epsdice-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsdice-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'epsdice-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'epsdice-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'epsdice-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'epsdice-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'epsdice-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsdice-cmd', + score: 0.004719094298848707 + } + ], + apptools: [ + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'apptools-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\AtAppendix{}', + snippet: '\\AtAppendix{$1}', + meta: 'apptools-cmd', + score: 8.82390883984482e-6 + } + ], + letltxmacro: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'letltxmacro-cmd', + score: 0.008565354665444157 + } + ], + menukeys: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'menukeys-cmd', + score: 0.354445763583904 + }, + { + caption: '\\adjustbox{}{}', + snippet: '\\adjustbox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.002008185536556013 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'menukeys-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'menukeys-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\usepackage{}', + snippet: '\\usepackage{$1}', + meta: 'menukeys-cmd', + score: 5.427890758130527 + }, + { + caption: '\\usepackage[]{}', + snippet: '\\usepackage[$1]{$2}', + meta: 'menukeys-cmd', + score: 5.427890758130527 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'menukeys-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'menukeys-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'menukeys-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'menukeys-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'menukeys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\mathlarger{}', + snippet: '\\mathlarger{$1}', + meta: 'menukeys-cmd', + score: 0.0031475241540308316 + }, + { + caption: '\\smaller', + snippet: '\\smaller', + meta: 'menukeys-cmd', + score: 0.001271007880944704 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'menukeys-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'menukeys-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'menukeys-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'menukeys-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'menukeys-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'menukeys-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'menukeys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'menukeys-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'menukeys-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'menukeys-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'menukeys-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'menukeys-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'menukeys-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'menukeys-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'menukeys-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'menukeys-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'menukeys-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'menukeys-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'menukeys-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'menukeys-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'menukeys-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'menukeys-cmd', + score: 0.2864294797053033 + } + ], + hypdvips: [ + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'hypdvips-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'hypdvips-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'hypdvips-cmd', + score: 7.849662248028187 + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'hypdvips-cmd', + score: 0.9202908262245683 + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'hypdvips-cmd', + score: 7.847906405228455 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'hypdvips-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'hypdvips-cmd', + score: 0.006609629561859019 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'hypdvips-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'hypdvips-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'hypdvips-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'hypdvips-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'hypdvips-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'hypdvips-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'hypdvips-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'hypdvips-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'hypdvips-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'hypdvips-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'hypdvips-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'hypdvips-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'hypdvips-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'hypdvips-cmd', + score: 0.009472569279662113 + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.006492248863367502 + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'hypdvips-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'hypdvips-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'hypdvips-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hypdvips-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'hypdvips-cmd', + score: 0.017289599800633146 + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'hypdvips-cmd', + score: 0.001509072212764015 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'hypdvips-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'hypdvips-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'hypdvips-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'hypdvips-cmd', + score: 7.849662248028187 + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.00978652043902115 + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'hypdvips-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'hypdvips-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'hypdvips-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'hypdvips-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'hypdvips-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'hypdvips-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'hypdvips-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.00029737672328168955 + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.0073781967296121 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'hypdvips-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'hypdvips-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'hypdvips-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'hypdvips-cmd', + score: 0.019788865471151957 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'hypdvips-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'hypdvips-cmd', + score: 3.800886892251021 + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'hypdvips-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'hypdvips-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'hypdvips-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'hypdvips-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'hypdvips-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'hypdvips-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'hypdvips-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'hypdvips-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.009652820108904094 + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'hypdvips-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'hypdvips-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'hypdvips-cmd', + score: 0.13586474005868793 + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.27111130260612365 + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'hypdvips-cmd', + score: 0.0038703587462843594 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'hypdvips-cmd', + score: 0.03741172773691362 + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'hypdvips-cmd', + score: 0.0004995635515943437 + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'hypdvips-cmd', + score: 7.847906405228455 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'hypdvips-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'hypdvips-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'hypdvips-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'hypdvips-cmd', + score: 6.006262128895586e-5 + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'hypdvips-cmd', + score: 0.00017906650306643613 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'hypdvips-cmd', + score: 1.4380093454211778 + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'hypdvips-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'hypdvips-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'hypdvips-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'hypdvips-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'hypdvips-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'hypdvips-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'hypdvips-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'hypdvips-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'hypdvips-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.004515152477030062 + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'hypdvips-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'hypdvips-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'hypdvips-cmd', + score: 0.3311721696201715 + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'hypdvips-cmd', + score: 0.06967310843464661 + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'hypdvips-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'hypdvips-cmd', + score: 0.9202908262245683 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.006492248863367502 + }, + { + caption: '\\bookmarkget{}', + snippet: '\\bookmarkget{$1}', + meta: 'hypdvips-cmd', + score: 0.00026847053008917257 + }, + { + caption: '\\bookmarksetup{}', + snippet: '\\bookmarksetup{$1}', + meta: 'hypdvips-cmd', + score: 0.001134118016265821 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'hypdvips-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'hypdvips-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'hypdvips-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'hypdvips-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'hypdvips-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'hypdvips-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'hypdvips-cmd', + score: 0.2864294797053033 + } + ], + easyReview: [ + { + caption: '\\highlight{}', + snippet: '\\highlight{$1}', + meta: 'easyReview-cmd', + score: 0.00021546602164732416 + }, + { + caption: '\\highlight', + snippet: '\\highlight', + meta: 'easyReview-cmd', + score: 0.00021546602164732416 + }, + { + caption: '\\alert{}', + snippet: '\\alert{$1}', + meta: 'easyReview-cmd', + score: 0.02756568949970745 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'easyReview-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'easyReview-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'easyReview-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'easyReview-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'easyReview-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'easyReview-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'easyReview-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'easyReview-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'easyReview-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'easyReview-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\missingfigure[]{}', + snippet: '\\missingfigure[$1]{$2}', + meta: 'easyReview-cmd', + score: 0.001558719179721163 + }, + { + caption: '\\missingfigure', + snippet: '\\missingfigure', + meta: 'easyReview-cmd', + score: 0.001558719179721163 + }, + { + caption: '\\todototoc', + snippet: '\\todototoc', + meta: 'easyReview-cmd', + score: 0.000325977535138643 + }, + { + caption: '\\todo{}', + snippet: '\\todo{$1}', + meta: 'easyReview-cmd', + score: 0.04115074278362878 + }, + { + caption: '\\todo[]{}', + snippet: '\\todo[$1]{$2}', + meta: 'easyReview-cmd', + score: 0.04115074278362878 + }, + { + caption: '\\todo', + snippet: '\\todo', + meta: 'easyReview-cmd', + score: 0.04115074278362878 + }, + { + caption: '\\listoftodos', + snippet: '\\listoftodos', + meta: 'easyReview-cmd', + score: 0.0005325975940754609 + }, + { + caption: '\\listoftodos[]', + snippet: '\\listoftodos[$1]', + meta: 'easyReview-cmd', + score: 0.0005325975940754609 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'easyReview-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'easyReview-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'easyReview-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'easyReview-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'easyReview-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'easyReview-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'easyReview-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'easyReview-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'easyReview-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'easyReview-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'easyReview-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareRobustCommand{}{}', + snippet: '\\DeclareRobustCommand{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.0010373158471650705 + }, + { + caption: '\\DeclareRobustCommand{}[]{}', + snippet: '\\DeclareRobustCommand{$1}[$2]{$3}', + meta: 'easyReview-cmd', + score: 0.0010373158471650705 + }, + { + caption: '\\sethlcolor{}', + snippet: '\\sethlcolor{$1}', + meta: 'easyReview-cmd', + score: 0.01970230898277056 + }, + { + caption: '\\st', + snippet: '\\st', + meta: 'easyReview-cmd', + score: 0.004652662833362787 + }, + { + caption: '\\st{}', + snippet: '\\st{$1}', + meta: 'easyReview-cmd', + score: 0.004652662833362787 + }, + { + caption: '\\def', + snippet: '\\def', + meta: 'easyReview-cmd', + score: 0.21357759092476175 + }, + { + caption: '\\hl{}', + snippet: '\\hl{$1}', + meta: 'easyReview-cmd', + score: 0.03421486301062431 + }, + { + caption: '\\sodef', + snippet: '\\sodef', + meta: 'easyReview-cmd', + score: 0.0017045357696831268 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\so', + snippet: '\\so', + meta: 'easyReview-cmd', + score: 0.004308800134587786 + }, + { + caption: '\\so{}', + snippet: '\\so{$1}', + meta: 'easyReview-cmd', + score: 0.004308800134587786 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'easyReview-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'easyReview-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'easyReview-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'easyReview-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'easyReview-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'easyReview-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'easyReview-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'easyReview-cmd', + score: 0.2864294797053033 + } + ], + quoting: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'quoting-cmd', + score: 0.413853376001159 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'quoting-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'quoting-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'quoting-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'quoting-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'quoting-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'quoting-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'quoting-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'quoting-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'quoting-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'quoting-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'quoting-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'quoting-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'quoting-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'quoting-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'quoting-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'quoting-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'quoting-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'quoting-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'quoting-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'quoting-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157 + } + ], + fouriernc: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fouriernc-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fouriernc-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fouriernc-cmd', + score: 0.021170869458413965 + } + ], + realboxes: [ + { + caption: '\\Rotatebox{}{}', + snippet: '\\Rotatebox{$1}{$2}', + meta: 'realboxes-cmd', + score: 1.8920528094586312e-5 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'realboxes-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'realboxes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'realboxes-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'realboxes-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'realboxes-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'realboxes-cmd', + score: 0.00107667147399019 + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'realboxes-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'realboxes-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.00015142240898356106 + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'realboxes-cmd', + score: 4.5350034239275855e-5 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'realboxes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'realboxes-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'realboxes-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'realboxes-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'realboxes-cmd', + score: 0.0018957469739775527 + } + ], + etextools: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'etextools-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'etextools-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'etextools-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'etextools-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'etextools-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'etextools-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'etextools-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'etextools-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'etextools-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'etextools-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'etextools-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'etextools-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'etextools-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etextools-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etextools-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'etextools-cmd', + score: 0.0018653410309739879 + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'etextools-cmd', + score: 0.00031058155311734754 + } + ], + ccaption: [ + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'ccaption-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'ccaption-cmd', + score: 1.897791904799601 + } + ], + exercise: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'exercise-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'exercise-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'exercise-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'exercise-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'exercise-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'exercise-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'exercise-cmd', + score: 0.00037306820619479756 + } + ], + slantsc: [ + { + caption: '\\scshape', + snippet: '\\scshape', + meta: 'slantsc-cmd', + score: 0.05364108855914402 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'slantsc-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'slantsc-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'slantsc-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'slantsc-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'slantsc-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'slantsc-cmd', + score: 0.0018957469739775527 + } + ], + 'glossary-longbooktabs': [ + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'glossary-longbooktabs-cmd', + score: 0.004974385202605165 + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'glossary-longbooktabs-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'glossary-longbooktabs-cmd', + score: 0.04533364657852219 + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'glossary-longbooktabs-cmd', + score: 0.07098077735912875 + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'glossary-longbooktabs-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'glossary-longbooktabs-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'glossary-longbooktabs-cmd', + score: 0.059857788139528495 + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'glossary-longbooktabs-cmd', + score: 0.0023853501147448834 + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'glossary-longbooktabs-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossary-longbooktabs-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'glossary-longbooktabs-cmd', + score: 9.952664522415981e-5 + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'glossary-longbooktabs-cmd', + score: 0.0016148498709822416 + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'glossary-longbooktabs-cmd', + score: 0.00044045261916551967 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'glossary-longbooktabs-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'glossary-longbooktabs-cmd', + score: 0.0029238994233674776 + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'glossary-longbooktabs-cmd', + score: 0.0313525090421608 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'glossary-longbooktabs-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'glossary-longbooktabs-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'glossary-longbooktabs-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossary-longbooktabs-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'glossary-longbooktabs-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'glossary-longbooktabs-cmd', + score: 0.018615449342361392 + } + ], + pgflibraryarrows: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgflibraryarrows-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgflibraryarrows-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryarrows-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryarrows-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgflibraryarrows-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryarrows-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryarrows-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryarrows-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.2864294797053033 + } + ], + soulpos: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'soulpos-cmd', + score: 0.00037306820619479756 + } + ], + gmp: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'gmp-cmd', + score: 0.413853376001159 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gmp-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gmp-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'gmp-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'gmp-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'gmp-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'gmp-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'gmp-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'gmp-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'gmp-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'gmp-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'gmp-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'gmp-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'gmp-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gmp-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'gmp-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'gmp-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'gmp-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'gmp-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'gmp-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'gmp-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'gmp-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'gmp-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'gmp-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'gmp-cmd', + score: 0.021170869458413965 + } + ], + csvsimple: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'csvsimple-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'csvsimple-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'csvsimple-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'csvsimple-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'csvsimple-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'csvsimple-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'csvsimple-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'csvsimple-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'csvsimple-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'csvsimple-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'csvsimple-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'csvsimple-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'csvsimple-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'csvsimple-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'csvsimple-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'csvsimple-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'csvsimple-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'csvsimple-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'csvsimple-cmd', + score: 0.008565354665444157 + } + ], + ebgaramond: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ebgaramond-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ebgaramond-cmd', + score: 0.008565354665444157 + } + ], + boldline: [ + { + caption: '\\hlineB{}', + snippet: '\\hlineB{$1}', + meta: 'boldline-cmd', + score: 0.0009735563258863602 + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'boldline-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'boldline-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'boldline-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'boldline-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'boldline-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'boldline-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'boldline-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'boldline-cmd', + score: 0.018615449342361392 + } + ], + fontaxes: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fontaxes-cmd', + score: 0.008565354665444157 + } + ], + pbsi: [ + { + caption: '\\bsifamily', + snippet: '\\bsifamily', + meta: 'pbsi-cmd', + score: 3.140504277052775e-5 + } + ], + 'tikz-qtree-compat': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-qtree-compat-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-compat-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-compat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-compat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-compat-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-compat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.2864294797053033 + } + ], + 'ebgaramond-maths': [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ebgaramond-maths-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ebgaramond-maths-cmd', + score: 0.008565354665444157 + } + ], + complexity: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'complexity-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'complexity-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'complexity-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'complexity-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'complexity-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'complexity-cmd', + score: 0.0018957469739775527 + } + ], + everysel: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'everysel-cmd', + score: 0.04598628699063736 + } + ], + txfontsb: [ + { + caption: '\\sqrt{}', + snippet: '\\sqrt{$1}', + meta: 'txfontsb-cmd', + score: 0.20240160977404634 + } + ], + nath: [ + { + caption: '\\vert', + snippet: '\\vert', + meta: 'nath-cmd', + score: 0.05152912629788525 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'nath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\quad', + snippet: '\\quad', + meta: 'nath-cmd', + score: 0.15242755832392743 + }, + { + caption: '\\underbrace{}', + snippet: '\\underbrace{$1}', + meta: 'nath-cmd', + score: 0.010373780436850907 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'nath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\delimgrowth', + snippet: '\\delimgrowth', + meta: 'nath-cmd', + score: 1.8073688234300064e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'nath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'nath-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\underline{}', + snippet: '\\underline{$1}', + meta: 'nath-cmd', + score: 0.14748550887002482 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'nath-cmd', + score: 1.897791904799601 + }, + { + caption: '\\qquad', + snippet: '\\qquad', + meta: 'nath-cmd', + score: 0.0878145577017131 + } + ], + vietnam: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'vietnam-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'vietnam-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'vietnam-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'vietnam-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'vietnam-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'vietnam-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'vietnam-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'vietnam-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'vietnam-cmd', + score: 0.021170869458413965 + } + ], + answers: [ + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'answers-cmd', + score: 0.0022216421267780076 + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'answers-cmd', + score: 0.0072203369120285256 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'answers-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'answers-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'answers-cmd', + score: 0.413853376001159 + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'answers-cmd', + score: 0.0024547099784948665 + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'answers-cmd', + score: 0.0024547099784948665 + } + ], + attachfile: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'attachfile-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'attachfile-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'attachfile-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'attachfile-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'attachfile-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'attachfile-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'attachfile-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'attachfile-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'attachfile-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'attachfile-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'attachfile-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'attachfile-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'attachfile-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'attachfile-cmd', + score: 0.009472569279662113 + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.006492248863367502 + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'attachfile-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'attachfile-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'attachfile-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'attachfile-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'attachfile-cmd', + score: 0.017289599800633146 + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'attachfile-cmd', + score: 0.001509072212764015 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'attachfile-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'attachfile-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'attachfile-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'attachfile-cmd', + score: 7.849662248028187 + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.00978652043902115 + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'attachfile-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'attachfile-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'attachfile-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'attachfile-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'attachfile-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'attachfile-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'attachfile-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.00029737672328168955 + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.0073781967296121 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'attachfile-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'attachfile-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'attachfile-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'attachfile-cmd', + score: 0.019788865471151957 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'attachfile-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'attachfile-cmd', + score: 3.800886892251021 + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'attachfile-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'attachfile-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'attachfile-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'attachfile-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'attachfile-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'attachfile-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'attachfile-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'attachfile-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.009652820108904094 + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'attachfile-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'attachfile-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'attachfile-cmd', + score: 0.13586474005868793 + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'attachfile-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'attachfile-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.27111130260612365 + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'attachfile-cmd', + score: 0.0038703587462843594 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'attachfile-cmd', + score: 0.03741172773691362 + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'attachfile-cmd', + score: 0.0004995635515943437 + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'attachfile-cmd', + score: 7.847906405228455 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'attachfile-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'attachfile-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'attachfile-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'attachfile-cmd', + score: 6.006262128895586e-5 + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'attachfile-cmd', + score: 0.00017906650306643613 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'attachfile-cmd', + score: 1.4380093454211778 + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'attachfile-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'attachfile-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'attachfile-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'attachfile-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'attachfile-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'attachfile-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'attachfile-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'attachfile-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'attachfile-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'attachfile-cmd', + score: 0.004515152477030062 + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'attachfile-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'attachfile-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'attachfile-cmd', + score: 0.3311721696201715 + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'attachfile-cmd', + score: 0.06967310843464661 + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'attachfile-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'attachfile-cmd', + score: 0.9202908262245683 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'attachfile-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'attachfile-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'attachfile-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'attachfile-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'attachfile-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'attachfile-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'attachfile-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'attachfile-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'attachfile-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411 + } + ], + doc: [ + { + caption: '\\do', + snippet: '\\do', + meta: 'doc-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\verb', + snippet: '\\verb', + meta: 'doc-cmd', + score: 0.1323269725886312 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'doc-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'doc-cmd', + score: 0.0072203369120285256 + } + ], + 'tkz-fct': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-fct-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-fct-cmd', + score: 0.0018653410309739879 + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-fct-cmd', + score: 0.00031058155311734754 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-fct-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-fct-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-fct-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-fct-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-fct-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-fct-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-fct-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-fct-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-fct-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-fct-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-fct-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-fct-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-fct-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-fct-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-fct-cmd', + score: 0.2864294797053033 + } + ], + notes2bib: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'notes2bib-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'notes2bib-cmd', + score: 0.2864294797053033 + } + ], + stackengine: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'stackengine-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'stackengine-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'stackengine-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'stackengine-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'stackengine-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'stackengine-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'stackengine-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'stackengine-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'stackengine-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'stackengine-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'stackengine-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'stackengine-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'stackengine-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'stackengine-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'stackengine-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'stackengine-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'stackengine-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'stackengine-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'stackengine-cmd', + score: 0.008565354665444157 + } + ], + cellspace: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'cellspace-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'cellspace-cmd', + score: 0.5473606021405326 + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'cellspace-cmd', + score: 2.650484574842396e-5 + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'cellspace-cmd', + score: 0.014532521139459619 + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'cellspace-cmd', + score: 0.0005078239917067089 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cellspace-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'cellspace-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.018615449342361392 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'cellspace-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'cellspace-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'cellspace-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'cellspace-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'cellspace-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'cellspace-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'cellspace-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'cellspace-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'cellspace-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'cellspace-cmd', + score: 0.028955796305270766 + } + ], + zxjatype: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'zxjatype-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'zxjatype-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'zxjatype-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'zxjatype-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'zxjatype-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'zxjatype-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'zxjatype-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'zxjatype-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'zxjatype-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'zxjatype-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zxjatype-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'zxjatype-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'zxjatype-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zxjatype-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'zxjatype-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'zxjatype-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'zxjatype-cmd', + score: 0.2864294797053033 + } + ], + newclude: [ + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'newclude-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'newclude-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\include{}', + snippet: '\\include{$1}', + meta: 'newclude-cmd', + score: 0.1547080054979312 + } + ], + 'pgf-umlcd': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlcd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgf-umlcd-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgf-umlcd-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlcd-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlcd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgf-umlcd-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlcd-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlcd-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.2864294797053033 + } + ], + 'thm-listof': [ + { + caption: '\\listtheoremname', + snippet: '\\listtheoremname', + meta: 'thm-listof-cmd', + score: 1.9443373798666845e-5 + }, + { + caption: '\\thmtformatoptarg', + snippet: '\\thmtformatoptarg', + meta: 'thm-listof-cmd', + score: 6.353668036093916e-5 + }, + { + caption: '\\listoftheorems[]', + snippet: '\\listoftheorems[$1]', + meta: 'thm-listof-cmd', + score: 1.9443373798666845e-5 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-listof-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thm-listof-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thm-listof-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thm-listof-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-listof-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-listof-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-listof-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-listof-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-listof-cmd', + score: 0.215689795055434 + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-listof-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-listof-cmd', + score: 0.0006133100544751855 + } + ], + 'thm-autoref': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-autoref-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-autoref-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-autoref-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-autoref-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-autoref-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-autoref-cmd', + score: 0.215689795055434 + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-autoref-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-autoref-cmd', + score: 0.0006133100544751855 + } + ], + 'thm-patch': [ + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-patch-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-patch-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-patch-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-patch-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-patch-cmd', + score: 0.215689795055434 + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-patch-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-patch-cmd', + score: 0.0006133100544751855 + } + ], + 'thm-kv': [ + { + caption: '\\declaretheoremstyle[]{}', + snippet: '\\declaretheoremstyle[$1]{$2}', + meta: 'thm-kv-cmd', + score: 0.0001168034231635369 + }, + { + caption: '\\declaretheorem[]{}', + snippet: '\\declaretheorem[$1]{$2}', + meta: 'thm-kv-cmd', + score: 0.0004904790216915127 + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'thm-kv-cmd', + score: 0.02533412165007986 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-kv-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-kv-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-kv-cmd', + score: 0.000701497773639073 + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-kv-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-kv-cmd', + score: 0.215689795055434 + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-kv-cmd', + score: 0.215689795055434 + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-kv-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-kv-cmd', + score: 0.0006133100544751855 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thm-kv-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thm-kv-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thm-kv-cmd', + score: 0.008565354665444157 + } + ], + onlyamsmath: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'onlyamsmath-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'onlyamsmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'onlyamsmath-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'onlyamsmath-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'onlyamsmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'onlyamsmath-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'onlyamsmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'onlyamsmath-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'onlyamsmath-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'onlyamsmath-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'onlyamsmath-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'onlyamsmath-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'onlyamsmath-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'onlyamsmath-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'onlyamsmath-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'onlyamsmath-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'onlyamsmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'onlyamsmath-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'onlyamsmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'onlyamsmath-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'onlyamsmath-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'onlyamsmath-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'onlyamsmath-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'onlyamsmath-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'onlyamsmath-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'onlyamsmath-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'onlyamsmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'onlyamsmath-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'onlyamsmath-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'onlyamsmath-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'onlyamsmath-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'onlyamsmath-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'onlyamsmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'onlyamsmath-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'onlyamsmath-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'onlyamsmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'onlyamsmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'onlyamsmath-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'onlyamsmath-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'onlyamsmath-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'onlyamsmath-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'onlyamsmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'onlyamsmath-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'onlyamsmath-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'onlyamsmath-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'onlyamsmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'onlyamsmath-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'onlyamsmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'onlyamsmath-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'onlyamsmath-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'onlyamsmath-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'onlyamsmath-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'onlyamsmath-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'onlyamsmath-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'onlyamsmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'onlyamsmath-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'onlyamsmath-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'onlyamsmath-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'onlyamsmath-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'onlyamsmath-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'onlyamsmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'onlyamsmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'onlyamsmath-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'onlyamsmath-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'onlyamsmath-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'onlyamsmath-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'onlyamsmath-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'onlyamsmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'onlyamsmath-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'onlyamsmath-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'onlyamsmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'onlyamsmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'onlyamsmath-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'onlyamsmath-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'onlyamsmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'onlyamsmath-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'onlyamsmath-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'onlyamsmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'onlyamsmath-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'onlyamsmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'onlyamsmath-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'onlyamsmath-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'onlyamsmath-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'onlyamsmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'onlyamsmath-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'onlyamsmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'onlyamsmath-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'onlyamsmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'onlyamsmath-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'onlyamsmath-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'onlyamsmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'onlyamsmath-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'onlyamsmath-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'onlyamsmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'onlyamsmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'onlyamsmath-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'onlyamsmath-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'onlyamsmath-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'onlyamsmath-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'onlyamsmath-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'onlyamsmath-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'onlyamsmath-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'onlyamsmath-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'onlyamsmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'onlyamsmath-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'onlyamsmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'onlyamsmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'onlyamsmath-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'onlyamsmath-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'onlyamsmath-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'onlyamsmath-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'onlyamsmath-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'onlyamsmath-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'onlyamsmath-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'onlyamsmath-cmd', + score: 0.0063276692758974925 + } + ], + arsclassica: [ + { + caption: '\\spacedlowsmallcaps{}', + snippet: '\\spacedlowsmallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.002677188251799468 + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'arsclassica-cmd', + score: 0.005008938879210868 + }, + { + caption: '\\spacedallcaps{}', + snippet: '\\spacedallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.0015281000475958944 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'arsclassica-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'arsclassica-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'arsclassica-cmd', + score: 0.004974385202605165 + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'arsclassica-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'arsclassica-cmd', + score: 0.01894952272365088 + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'arsclassica-cmd', + score: 0.04533364657852219 + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'arsclassica-cmd', + score: 0.07098077735912875 + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'arsclassica-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'arsclassica-cmd', + score: 0.005865460617491447 + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'arsclassica-cmd', + score: 0.059857788139528495 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.018348594199161503 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'arsclassica-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'arsclassica-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'arsclassica-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'arsclassica-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'arsclassica-cmd', + score: 0.422097569591803 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'arsclassica-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'arsclassica-cmd', + score: 0.3147206476372336 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'arsclassica-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'arsclassica-cmd', + score: 1.897791904799601 + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'arsclassica-cmd', + score: 5.806935368083486e-5 + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'arsclassica-cmd', + score: 0.42355747798114207 + }, + { + caption: '\\titleclass{}{}[]', + snippet: '\\titleclass{$1}{$2}[$3]', + meta: 'arsclassica-cmd', + score: 0.00028979763314974667 + }, + { + caption: '\\titlelabel{}', + snippet: '\\titlelabel{$1}', + meta: 'arsclassica-cmd', + score: 6.40387839367932e-6 + }, + { + caption: '\\thetitle', + snippet: '\\thetitle', + meta: 'arsclassica-cmd', + score: 0.0015531478302713473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'arsclassica-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'arsclassica-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\titleformat{}{}{}{}{}[]', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]', + meta: 'arsclassica-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}[]{}{}{}{}', + snippet: '\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'arsclassica-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}{}', + snippet: '\\titleformat{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titleformat{}{}{}{}{}', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}', + meta: 'arsclassica-cmd', + score: 0.03475519439740096 + }, + { + caption: '\\titlespacing{}{}{}{}', + snippet: '\\titlespacing{$1}{$2}{$3}{$4}', + meta: 'arsclassica-cmd', + score: 0.023062744385192156 + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'arsclassica-cmd', + score: 0.038323601301945065 + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'arsclassica-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.007138622674767024 + }, + { + caption: '\\filleft', + snippet: '\\filleft', + meta: 'arsclassica-cmd', + score: 7.959989906732799e-5 + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'arsclassica-cmd', + score: 0.0004835660211260246 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'arsclassica-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'arsclassica-cmd', + score: 0.044016804142963585 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'arsclassica-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\chaptertitlename', + snippet: '\\chaptertitlename', + meta: 'arsclassica-cmd', + score: 0.0016985007766926272 + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'arsclassica-cmd', + score: 0.3277033727934986 + }, + { + caption: '\\filright', + snippet: '\\filright', + meta: 'arsclassica-cmd', + score: 7.959989906732799e-5 + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'arsclassica-cmd', + score: 0.019273712561461216 + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.019273712561461216 + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0003890810058478364 + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0004717618449370015 + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'arsclassica-cmd', + score: 5.0133404990680195e-5 + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'arsclassica-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'arsclassica-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'arsclassica-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'arsclassica-cmd', + score: 0.00015256647321237863 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'arsclassica-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'arsclassica-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'arsclassica-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'arsclassica-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\marginpar{}', + snippet: '\\marginpar{$1}', + meta: 'arsclassica-cmd', + score: 0.003400158497921723 + }, + { + caption: '\\marginpar', + snippet: '\\marginpar', + meta: 'arsclassica-cmd', + score: 0.003400158497921723 + }, + { + caption: '\\cftsecleader', + snippet: '\\cftsecleader', + meta: 'arsclassica-cmd', + score: 0.0011340882025681251 + }, + { + caption: '\\cftsubsecleader', + snippet: '\\cftsubsecleader', + meta: 'arsclassica-cmd', + score: 1.0644172549700836e-5 + }, + { + caption: '\\spacedlowsmallcaps{}', + snippet: '\\spacedlowsmallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.002677188251799468 + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'arsclassica-cmd', + score: 0.005008938879210868 + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'arsclassica-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'arsclassica-cmd', + score: 0.005924520024686584 + }, + { + caption: '\\part{}', + snippet: '\\part{$1}', + meta: 'arsclassica-cmd', + score: 0.022180129487444723 + }, + { + caption: '\\tocEntry{}', + snippet: '\\tocEntry{$1}', + meta: 'arsclassica-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\graffito{}', + snippet: '\\graffito{$1}', + meta: 'arsclassica-cmd', + score: 1.1006799670632527e-5 + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'arsclassica-cmd', + score: 0.422097569591803 + }, + { + caption: '\\spacedallcaps{}', + snippet: '\\spacedallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.0015281000475958944 + }, + { + caption: '\\cftchapleader', + snippet: '\\cftchapleader', + meta: 'arsclassica-cmd', + score: 1.0644172549700836e-5 + }, + { + caption: '\\myVersion', + snippet: '\\myVersion', + meta: 'arsclassica-cmd', + score: 0.00018029288638573757 + }, + { + caption: '\\ctparttext{}', + snippet: '\\ctparttext{$1}', + meta: 'arsclassica-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0008555564394100388 + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.012985816912639263 + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'arsclassica-cmd', + score: 0.000396664302361659 + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'arsclassica-cmd', + score: 2.341195220791228 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'arsclassica-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'arsclassica-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'arsclassica-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'arsclassica-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'arsclassica-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'arsclassica-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'arsclassica-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'arsclassica-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\lsstyle', + snippet: '\\lsstyle', + meta: 'arsclassica-cmd', + score: 0.0023367519914345774 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'arsclassica-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\DisableLigatures[]{}', + snippet: '\\DisableLigatures[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.0009805246614299932 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'arsclassica-cmd', + score: 0.00021116765384691477 + } + ], + blkarray: [ + { + caption: '\\small', + snippet: '\\small', + meta: 'blkarray-cmd', + score: 0.2447632045426295 + }, + { + caption: '\\small{}', + snippet: '\\small{$1}', + meta: 'blkarray-cmd', + score: 0.2447632045426295 + } + ], + 'tkz-tab': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-tab-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-tab-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-tab-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-tab-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-tab-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-tab-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-tab-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-tab-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tkz-tab-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tkz-tab-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tkz-tab-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tkz-tab-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-tab-cmd', + score: 0.0018653410309739879 + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-tab-cmd', + score: 0.00031058155311734754 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-tab-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-tab-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-tab-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-tab-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-tab-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-tab-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-tab-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-tab-cmd', + score: 0.2864294797053033 + } + ], + todo: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'todo-cmd', + score: 0.0017966000518546787 + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'todo-cmd', + score: 0.025060530944368123 + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'todo-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'todo-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'todo-cmd', + score: 0.0006671850995492977 + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'todo-cmd', + score: 0.0006671850995492977 + } + ], + lcg: [ + { + caption: '\\rand', + snippet: '\\rand', + meta: 'lcg-cmd', + score: 6.2350576842596716e-6 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'lcg-cmd', + score: 0.00037306820619479756 + } + ], + kantlipsum: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'kantlipsum-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'kantlipsum-cmd', + score: 0.2864294797053033 + } + ], + chappg: [ + { + caption: '\\pagenumbering{}', + snippet: '\\pagenumbering{$1}', + meta: 'chappg-cmd', + score: 0.06731737633021802 + } + ], + chessboard: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'chessboard-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'chessboard-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboardfontencoding{}', + snippet: '\\setboardfontencoding{$1}', + meta: 'chessboard-cmd', + score: 0.00014668111964632249 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chessboard-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chessboard-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chessboard-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chessboard-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chessboard-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chessboard-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chessboard-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chessboard-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chessboard-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chessboard-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chessboard-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chessboard-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chessboard-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chessboard-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chessboard-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chessboard-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chessboard-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chessboard-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chessboard-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chessboard-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'chessboard-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chessboard-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chessboard-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'chessboard-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'chessboard-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'chessboard-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'chessboard-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'chessboard-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'chessboard-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'chessboard-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'chessboard-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'chessboard-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'chessboard-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'chessboard-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'chessboard-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'chessboard-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chessboard-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chessboard-cmd', + score: 0.008565354665444157 + } + ], + xskak: [ + { + caption: '\\mainline{}', + snippet: '\\mainline{$1}', + meta: 'xskak-cmd', + score: 0.0010267678375242572 + }, + { + caption: '\\newchessgame', + snippet: '\\newchessgame', + meta: 'xskak-cmd', + score: 0.000880086717877935 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xskak-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xskak-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xskak-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboardfontencoding{}', + snippet: '\\setboardfontencoding{$1}', + meta: 'xskak-cmd', + score: 0.00014668111964632249 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xskak-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xskak-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xskak-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xskak-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xskak-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xskak-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xskak-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xskak-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xskak-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xskak-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xskak-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xskak-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xskak-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xskak-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xskak-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xskak-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xskak-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xskak-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xskak-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xskak-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xskak-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'xskak-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'xskak-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'xskak-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xskak-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xskak-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'xskak-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'xskak-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'xskak-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'xskak-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xskak-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'xskak-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'xskak-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xskak-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'xskak-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xskak-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xskak-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'xskak-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'xskak-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'xskak-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'xskak-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xskak-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'xskak-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xskak-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'xskak-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'xskak-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'xskak-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xskak-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'xskak-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'xskak-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'xskak-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'xskak-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'xskak-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'xskak-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'xskak-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'xskak-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'xskak-cmd', + score: 0.006520475264573554 + } + ], + pgfheaps: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfheaps-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfheaps-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfheaps-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfheaps-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfheaps-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfheaps-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfheaps-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfheaps-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfheaps-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfheaps-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfheaps-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfheaps-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfheaps-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfheaps-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfheaps-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfheaps-cmd', + score: 0.2864294797053033 + } + ], + pgfshade: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfshade-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfshade-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfshade-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfshade-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfshade-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfshade-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfshade-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfshade-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfshade-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfshade-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfshade-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfshade-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfshade-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfshade-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfshade-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfshade-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfshade-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfshade-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfshade-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfshade-cmd', + score: 0.2864294797053033 + } + ], + showframe: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'showframe-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'showframe-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'showframe-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'showframe-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'showframe-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'showframe-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'showframe-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'showframe-cmd', + score: 0.000325977535138643 + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'showframe-cmd', + score: 0.0008957666085644653 + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'showframe-cmd', + score: 0.0003608141410278152 + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'showframe-cmd', + score: 0.0007216282820556304 + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'showframe-cmd', + score: 0.0017658629469099734 + } + ], + psvectorian: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psvectorian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'psvectorian-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'psvectorian-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'psvectorian-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'psvectorian-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'psvectorian-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'psvectorian-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'psvectorian-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'psvectorian-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psvectorian-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'psvectorian-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'psvectorian-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'psvectorian-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'psvectorian-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'psvectorian-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'psvectorian-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'psvectorian-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'psvectorian-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'psvectorian-cmd', + score: 0.006520475264573554 + } + ], + 'pst-grad': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-grad-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-grad-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-grad-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-grad-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-grad-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-grad-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-grad-cmd', + score: 0.006520475264573554 + } + ], + cool: [ + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'cool-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'cool-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'cool-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'cool-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'cool-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'cool-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'cool-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'cool-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'cool-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'cool-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'cool-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'cool-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'cool-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'cool-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'cool-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'cool-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'cool-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'cool-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'cool-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'cool-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'cool-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'cool-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'cool-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'cool-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'cool-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'cool-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'cool-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'cool-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'cool-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'cool-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'cool-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'cool-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'cool-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'cool-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'cool-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'cool-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'cool-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'cool-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'cool-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'cool-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'cool-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'cool-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'cool-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'cool-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'cool-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'cool-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'cool-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'cool-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'cool-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'cool-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'cool-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'cool-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'cool-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'cool-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'cool-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'cool-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'cool-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'cool-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'cool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'cool-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'cool-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'cool-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'cool-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'cool-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'cool-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'cool-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'cool-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'cool-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'cool-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'cool-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'cool-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'cool-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'cool-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'cool-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'cool-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'cool-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'cool-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'cool-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'cool-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'cool-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'cool-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'cool-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'cool-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'cool-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'cool-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'cool-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'cool-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'cool-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'cool-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'cool-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'cool-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'cool-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'cool-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'cool-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'cool-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'cool-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'cool-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'cool-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'cool-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'cool-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'cool-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'cool-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'cool-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'cool-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'cool-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'cool-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'cool-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'cool-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'cool-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'cool-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'cool-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'cool-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'cool-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'cool-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'cool-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'cool-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'cool-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'cool-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'cool-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'cool-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'cool-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'cool-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'cool-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'cool-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'cool-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'cool-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'cool-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'cool-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'cool-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'cool-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'cool-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'cool-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'cool-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'cool-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'cool-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'cool-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'cool-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'cool-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'cool-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'cool-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'cool-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'cool-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'cool-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'cool-cmd', + score: 0.0017966000518546787 + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'cool-cmd', + score: 0.025060530944368123 + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'cool-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'cool-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'cool-cmd', + score: 0.0006671850995492977 + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'cool-cmd', + score: 0.0006671850995492977 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'cool-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'cool-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'cool-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cool-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'cool-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'cool-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'cool-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\forloop{}{}{}{}', + snippet: '\\forloop{$1}{$2}{$3}{$4}', + meta: 'cool-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'cool-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'cool-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'cool-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'cool-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'cool-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'cool-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'cool-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'cool-cmd', + score: 0.0063276692758974925 + } + ], + xassoccnt: [ + { + caption: '\\NewTotalDocumentCounter{}', + snippet: '\\NewTotalDocumentCounter{$1}', + meta: 'xassoccnt-cmd', + score: 1.5075186740106946e-5 + }, + { + caption: '\\DeclareAssociatedCounters{}{}', + snippet: '\\DeclareAssociatedCounters{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 1.5075186740106946e-5 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xassoccnt-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'xassoccnt-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'xassoccnt-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xassoccnt-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'xassoccnt-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xassoccnt-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'xassoccnt-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'xassoccnt-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xassoccnt-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'xassoccnt-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xassoccnt-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xassoccnt-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xassoccnt-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xassoccnt-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xassoccnt-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xassoccnt-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xassoccnt-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xassoccnt-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033 + } + ], + chemscheme: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'chemscheme-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemscheme-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemscheme-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemscheme-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chemscheme-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chemscheme-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chemscheme-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chemscheme-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chemscheme-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemscheme-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chemscheme-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chemscheme-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemscheme-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemscheme-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemscheme-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chemscheme-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'chemscheme-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemscheme-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'chemscheme-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemscheme-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemscheme-cmd', + score: 0.021170869458413965 + } + ], + 'pst-all': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-all-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-all-cmd', + score: 0.0016005722621532548 + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-all-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-all-cmd', + score: 1.4425339817971206 + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-all-cmd', + score: 0.0005786730478266738 + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-all-cmd', + score: 0.006520475264573554 + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-all-cmd', + score: 0.006520475264573554 + } + ], + regexpatch: [ + { + caption: '\\xpatchcmd{}{}{}{}{}', + snippet: '\\xpatchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'regexpatch-cmd', + score: 0.0019344877752147675 + }, + { + caption: '\\xpatchcmd', + snippet: '\\xpatchcmd', + meta: 'regexpatch-cmd', + score: 0.0019344877752147675 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'regexpatch-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'regexpatch-cmd', + score: 0.2864294797053033 + } + ], + chronosys: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronosys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronosys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronosys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chronosys-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chronosys-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chronosys-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronosys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chronosys-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronosys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chronosys-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronosys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chronosys-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronosys-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chronosys-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronosys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronosys-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chronosys-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronosys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chronosys-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronosys-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chronosys-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chronosys-cmd', + score: 0.2864294797053033 + } + ], + newfloat: [ + { + caption: '\\DeclareFloatingEnvironment[]{}', + snippet: '\\DeclareFloatingEnvironment[$1]{$2}', + meta: 'newfloat-cmd', + score: 2.603029874713569e-5 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'newfloat-cmd', + score: 0.00037306820619479756 + } + ], + zref: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'zref-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'zref-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927 + } + ], + bmpsize: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bmpsize-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bmpsize-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bmpsize-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'bmpsize-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'bmpsize-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'bmpsize-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'bmpsize-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'bmpsize-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bmpsize-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'bmpsize-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'bmpsize-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bmpsize-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bmpsize-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bmpsize-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'bmpsize-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bmpsize-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bmpsize-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'bmpsize-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'bmpsize-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bmpsize-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bmpsize-cmd', + score: 0.021170869458413965 + } + ], + steinmetz: [ + { + caption: '\\Line', + snippet: '\\Line', + meta: 'steinmetz-cmd', + score: 0.0006078790177929149 + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'steinmetz-cmd', + score: 0.0008987552240147395 + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'steinmetz-cmd', + score: 0.014519741542622297 + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'steinmetz-cmd', + score: 0.00022468880600368487 + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'steinmetz-cmd', + score: 0.002970308722584179 + } + ], + pageslts: [ + { + caption: '\\thepage', + snippet: '\\thepage', + meta: 'pageslts-cmd', + score: 0.0591555998103519 + }, + { + caption: '\\pagenumbering{}', + snippet: '\\pagenumbering{$1}', + meta: 'pageslts-cmd', + score: 0.06731737633021802 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'pageslts-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'pageslts-cmd', + score: 0.006609629561859019 + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'pageslts-cmd', + score: 0.010304996748556729 + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'pageslts-cmd', + score: 0.013774721817648336 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'pageslts-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'pageslts-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pageslts-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pageslts-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pageslts-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pageslts-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157 + } + ], + chronology: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chronology-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'chronology-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'chronology-cmd', + score: 0.354445763583904 + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'chronology-cmd', + score: 0.354445763583904 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronology-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronology-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'chronology-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'chronology-cmd', + score: 0.10068045662118841 + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'chronology-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'chronology-cmd', + score: 0.028955796305270766 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronology-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronology-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronology-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chronology-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chronology-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chronology-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronology-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chronology-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronology-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chronology-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronology-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronology-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chronology-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronology-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronology-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chronology-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chronology-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chronology-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronology-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronology-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chronology-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chronology-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chronology-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronology-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chronology-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chronology-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronology-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chronology-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chronology-cmd', + score: 0.2864294797053033 + } + ], + spreadtab: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'spreadtab-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'spreadtab-cmd', + score: 0.021170869458413965 + } + ], + algpascal: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algpascal-cmd', + score: 0.0019861803661869416 + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algpascal-cmd', + score: 0.008622777195102994 + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algpascal-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algpascal-cmd', + score: 0.0008685861525307122 + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algpascal-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algpascal-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algpascal-cmd', + score: 0.0030209395012065327 + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algpascal-cmd', + score: 0.005178604573219454 + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algpascal-cmd', + score: 0.00025315185701145097 + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algpascal-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algpascal-cmd', + score: 0.0024415580558825975 + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algpascal-cmd', + score: 0.0007916858220314837 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algpascal-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algpascal-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algpascal-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algpascal-cmd', + score: 0.0003102486920966127 + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algpascal-cmd', + score: 0.0005463612015579842 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algpascal-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algpascal-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algpascal-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algpascal-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algpascal-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algpascal-cmd', + score: 0.0018957469739775527 + } + ], + cabin: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'cabin-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cabin-cmd', + score: 0.008565354665444157 + } + ], + erewhon: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'erewhon-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'erewhon-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'erewhon-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'erewhon-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'erewhon-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'erewhon-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'erewhon-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'erewhon-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'erewhon-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'erewhon-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'erewhon-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'erewhon-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'erewhon-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'erewhon-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'erewhon-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'erewhon-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'erewhon-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'erewhon-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'erewhon-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'erewhon-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'erewhon-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'erewhon-cmd', + score: 0.008565354665444157 + } + ], + tgcursor: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgcursor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgcursor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgcursor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgcursor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgcursor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgcursor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgcursor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgcursor-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgcursor-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgcursor-cmd', + score: 0.021170869458413965 + } + ], + ifvtex: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ifvtex-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'ifvtex-cmd', + score: 0.002958865219480927 + } + ], + memhfixc: [ + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'memhfixc-cmd', + score: 1.2569477427490174 + } + ], + longfigure: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'longfigure-cmd', + score: 0.3277033727934986 + } + ], + lato: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'lato-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'lato-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'lato-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'lato-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'lato-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'lato-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\scshape', + snippet: '\\scshape', + meta: 'lato-cmd', + score: 0.05364108855914402 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'lato-cmd', + score: 0.00037306820619479756 + } + ], + authoraftertitle: [ + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'authoraftertitle-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'authoraftertitle-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'authoraftertitle-cmd', + score: 0.9202908262245683 + } + ], + listofsymbols: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'listofsymbols-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'listofsymbols-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'listofsymbols-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'listofsymbols-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'listofsymbols-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'listofsymbols-cmd', + score: 0.0018957469739775527 + } + ], + hvfloat: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hvfloat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.018348594199161503 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hvfloat-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'hvfloat-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hvfloat-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'hvfloat-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'hvfloat-cmd', + score: 0.422097569591803 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hvfloat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'hvfloat-cmd', + score: 0.3147206476372336 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'hvfloat-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'hvfloat-cmd', + score: 1.897791904799601 + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'hvfloat-cmd', + score: 5.806935368083486e-5 + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'hvfloat-cmd', + score: 0.42355747798114207 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hvfloat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hvfloat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'hvfloat-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'hvfloat-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'hvfloat-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'hvfloat-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hvfloat-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'hvfloat-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hvfloat-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'hvfloat-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'hvfloat-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.0003890810058478364 + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.0004717618449370015 + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'hvfloat-cmd', + score: 5.0133404990680195e-5 + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'hvfloat-cmd', + score: 0.0001872850414971473 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hvfloat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hvfloat-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hvfloat-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'hvfloat-cmd', + score: 0.00015256647321237863 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hvfloat-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'hvfloat-cmd', + score: 0.2253056071787701 + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'hvfloat-cmd', + score: 0.021473212893597875 + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'hvfloat-cmd', + score: 0.021473212893597875 + } + ], + thmbox: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thmbox-cmd', + score: 0.00037306820619479756 + } + ], + proba: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'proba-cmd', + score: 0.0017966000518546787 + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'proba-cmd', + score: 0.025060530944368123 + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'proba-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'proba-cmd', + score: 0.0014358547624941567 + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'proba-cmd', + score: 0.0006671850995492977 + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'proba-cmd', + score: 0.0006671850995492977 + } + ], + datatool: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'datatool-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'datatool-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'datatool-cmd', + score: 0.0017755897148012264 + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'datatool-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datatool-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'datatool-cmd', + score: 0.004209937150980285 + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'datatool-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'datatool-cmd', + score: 0.0011582952152188854 + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'datatool-cmd', + score: 0.0028650540724050534 + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'datatool-cmd', + score: 0.0035536135737312827 + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'datatool-cmd', + score: 0.0010893680553454854 + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'datatool-cmd', + score: 0.051980653969641216 + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'datatool-cmd', + score: 0.006473769486518971 + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'datatool-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'datatool-cmd', + score: 0.0054372322008878786 + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'datatool-cmd', + score: 0.000984722260624791 + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'datatool-cmd', + score: 0.0011508785476242003 + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'datatool-cmd', + score: 0.002995924112493351 + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'datatool-cmd', + score: 0.005709261168797874 + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'datatool-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'datatool-cmd', + score: 0.004163642482777231 + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'datatool-cmd', + score: 0.0006518541515279979 + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'datatool-cmd', + score: 0.05397545277891961 + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'datatool-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'datatool-cmd', + score: 0.0011773327219377148 + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'datatool-cmd', + score: 0.00322520920930312 + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'datatool-cmd', + score: 0.11946660537765894 + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'datatool-cmd', + score: 0.0011677288242806726 + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'datatool-cmd', + score: 0.42607994509619934 + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'datatool-cmd', + score: 0.0015607282046545064 + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'datatool-cmd', + score: 0.0003468284144579442 + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'datatool-cmd', + score: 0.0016498799924012809 + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'datatool-cmd', + score: 0.0847414497955395 + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'datatool-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'datatool-cmd', + score: 0.004820143328295316 + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'datatool-cmd', + score: 0.006765684097139381 + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'datatool-cmd', + score: 0.11585556755884258 + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'datatool-cmd', + score: 0.00011383372700282614 + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'datatool-cmd', + score: 2.3482915591834053e-5 + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'datatool-cmd', + score: 0.05613164277964739 + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'datatool-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'datatool-cmd', + score: 0.002459139437356601 + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'datatool-cmd', + score: 0.005931777024772073 + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'datatool-cmd', + score: 0.06345266254167037 + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'datatool-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'datatool-cmd', + score: 0.0015181439193121889 + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'datatool-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'datatool-cmd', + score: 0.022224283488673075 + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'datatool-cmd', + score: 0.04318078602869565 + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'datatool-cmd', + score: 0.012799893214578391 + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'datatool-cmd', + score: 0.0008555101484119994 + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'datatool-cmd', + score: 3.164631070474435e-5 + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'datatool-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'datatool-cmd', + score: 0.0037482529712850755 + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'datatool-cmd', + score: 1.4341091141105058 + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'datatool-cmd', + score: 3.423236656565836e-5 + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'datatool-cmd', + score: 0.021828316911576096 + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'datatool-cmd', + score: 1.3908704929884828e-5 + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'datatool-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'datatool-cmd', + score: 0.000347742918592393 + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5 + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'datatool-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'datatool-cmd', + score: 0.008197171096663127 + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'datatool-cmd', + score: 0.005300291684408929 + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'datatool-cmd', + score: 0.0016148076375871775 + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'datatool-cmd', + score: 8.477207854183949e-5 + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'datatool-cmd', + score: 0.02549889375975901 + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'datatool-cmd', + score: 0.00047859486202980376 + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'datatool-cmd', + score: 0.11280487530505384 + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'datatool-cmd', + score: 0.0005923542426657187 + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'datatool-cmd', + score: 6.625561928497235e-5 + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'datatool-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'datatool-cmd', + score: 0.002022594681005002 + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'datatool-cmd', + score: 2.7817409859769657e-5 + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'datatool-cmd', + score: 1.897791904799601 + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'datatool-cmd', + score: 0.013399422292458848 + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'datatool-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'datatool-cmd', + score: 3.5779964196240445e-5 + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'datatool-cmd', + score: 6.216218551413489e-5 + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'datatool-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'datatool-cmd', + score: 0.00024247684499275043 + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'datatool-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'datatool-cmd', + score: 0.015507614799858266 + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'datatool-cmd', + score: 0.007611544955294224 + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'datatool-cmd', + score: 0.050370758781422345 + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'datatool-cmd', + score: 0.0002851769278703356 + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'datatool-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'datatool-cmd', + score: 0.0004896780659212191 + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'datatool-cmd', + score: 0.013010882180364367 + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'datatool-cmd', + score: 0.0011096532692473691 + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'datatool-cmd', + score: 0.006800272303210672 + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'datatool-cmd', + score: 7.874446783586035e-5 + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'datatool-cmd', + score: 0.0058847868741168765 + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'datatool-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'datatool-cmd', + score: 0.0006435164702005918 + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'datatool-cmd', + score: 0.02181954887028883 + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'datatool-cmd', + score: 0.04116833357968482 + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'datatool-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'datatool-cmd', + score: 0.0015513861600956144 + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'datatool-cmd', + score: 0.0022415507993352067 + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'datatool-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'datatool-cmd', + score: 0.02404262443651467 + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'datatool-cmd', + score: 0.05285123457928509 + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'datatool-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'datatool-cmd', + score: 0.040463088537699636 + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'datatool-cmd', + score: 0.007190995792600074 + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'datatool-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'datatool-cmd', + score: 0.050370402546134785 + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'datatool-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'datatool-cmd', + score: 8.180643329881783e-5 + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'datatool-cmd', + score: 0.006176447465423192 + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'datatool-cmd', + score: 0.005640718203101287 + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'datatool-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'datatool-cmd', + score: 0.025366949660913504 + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'datatool-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'datatool-cmd', + score: 0.0008896391580266903 + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'datatool-cmd', + score: 0.002254008371792865 + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'datatool-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'datatool-cmd', + score: 0.002354950225950599 + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'datatool-cmd', + score: 0.00340470256994063 + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'datatool-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'datatool-cmd', + score: 0.001781687642431819 + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'datatool-cmd', + score: 0.002475379242338094 + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'datatool-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'datatool-cmd', + score: 0.0003640644365701238 + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'datatool-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'datatool-cmd', + score: 0.00025939638266884963 + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'datatool-cmd', + score: 6.204977642542802e-5 + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'datatool-cmd', + score: 0.048131780413380156 + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'datatool-cmd', + score: 0.000361814283649031 + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'datatool-cmd', + score: 0.005542465148816408 + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'datatool-cmd', + score: 0.0011971697553682045 + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'datatool-cmd', + score: 0.0038210003967178293 + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'datatool-cmd', + score: 0.03051120054363316 + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'datatool-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'datatool-cmd', + score: 0.010227440663206161 + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'datatool-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'datatool-cmd', + score: 0.0021229156376192525 + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'datatool-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'datatool-cmd', + score: 0.0007754886988089101 + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'datatool-cmd', + score: 0.029440493885398676 + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'datatool-cmd', + score: 0.00013963711107573638 + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'datatool-cmd', + score: 0.009355514755312534 + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'datatool-cmd', + score: 0.0005912636157903734 + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'datatool-cmd', + score: 0.0004286136584068833 + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'datatool-cmd', + score: 0.0030745841706804776 + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'datatool-cmd', + score: 0.010241823778997489 + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'datatool-cmd', + score: 0.3608680734736821 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datatool-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'datatool-cmd', + score: 0.019171182556792562 + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'datatool-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'datatool-cmd', + score: 0.18137737738638837 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'datatool-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'datatool-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'datatool-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'datatool-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'datatool-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'datatool-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'datatool-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'datatool-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'datatool-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'datatool-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'datatool-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datatool-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'datatool-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'datatool-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'datatool-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'datatool-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'datatool-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'datatool-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'datatool-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'datatool-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datatool-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datatool-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'datatool-cmd', + score: 0.0063276692758974925 + } + ], + fmtcount: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fmtcount-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fmtcount-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'fmtcount-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'fmtcount-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'fmtcount-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'fmtcount-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'fmtcount-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'fmtcount-cmd', + score: 0.002671974990314091 + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.00023171033119130004 + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 7.482069221111606e-5 + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.00035805058319299113 + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 0.00041307691354437894 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'fmtcount-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'fmtcount-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'fmtcount-cmd', + score: 0.0006607703576475988 + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.0006796212875843042 + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'fmtcount-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'fmtcount-cmd', + score: 0.002560998917940627 + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'fmtcount-cmd', + score: 8.860754525300578e-5 + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'fmtcount-cmd', + score: 0.00029867998381154486 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fmtcount-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'fmtcount-cmd', + score: 7.723677706376668e-5 + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'fmtcount-cmd', + score: 4.002553629215439e-5 + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.00028992557275763024 + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 0.00014933999190577243 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fmtcount-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'fmtcount-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'fmtcount-cmd', + score: 0.0063276692758974925 + } + ], + aurl: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'aurl-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'aurl-cmd', + score: 0.00047530324346933345 + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'aurl-cmd', + score: 0.0005277905480209891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'aurl-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'aurl-cmd', + score: 0.001030592515645366 + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'aurl-cmd', + score: 0.0002854206807593436 + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'aurl-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'aurl-cmd', + score: 0.0006882563723629154 + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'aurl-cmd', + score: 0.010515056688180681 + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'aurl-cmd', + score: 0.008041789461944983 + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'aurl-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'aurl-cmd', + score: 0.0032990580087398644 + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'aurl-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'aurl-cmd', + score: 3.7048287721105874e-5 + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'aurl-cmd', + score: 0.009472569279662113 + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'aurl-cmd', + score: 0.006492248863367502 + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'aurl-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'aurl-cmd', + score: 0.00014582556188448738 + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'aurl-cmd', + score: 0.006963729684667191 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'aurl-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'aurl-cmd', + score: 0.005553384455935491 + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'aurl-cmd', + score: 0.001042697111754002 + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'aurl-cmd', + score: 0.017289599800633146 + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'aurl-cmd', + score: 0.001509072212764015 + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'aurl-cmd', + score: 0.009278344180101056 + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'aurl-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'aurl-cmd', + score: 7.849662248028187 + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'aurl-cmd', + score: 7.849662248028187 + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'aurl-cmd', + score: 0.00978652043902115 + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'aurl-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'aurl-cmd', + score: 0.00012704528567339081 + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'aurl-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'aurl-cmd', + score: 0.00018777198999871106 + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'aurl-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'aurl-cmd', + score: 0.02873756018238537 + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'aurl-cmd', + score: 0.0200686676229443 + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'aurl-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'aurl-cmd', + score: 7.950698053641679e-5 + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'aurl-cmd', + score: 0.00029737672328168955 + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'aurl-cmd', + score: 0.0073781967296121 + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'aurl-cmd', + score: 0.002140559856649122 + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'aurl-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'aurl-cmd', + score: 0.01034327266194849 + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'aurl-cmd', + score: 0.019788865471151957 + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'aurl-cmd', + score: 3.800886892251021 + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'aurl-cmd', + score: 3.800886892251021 + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'aurl-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'aurl-cmd', + score: 0.2334089308452787 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'aurl-cmd', + score: 1.2569477427490174 + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'aurl-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'aurl-cmd', + score: 0.0019832324299155183 + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'aurl-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'aurl-cmd', + score: 0.007928096378157487 + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'aurl-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'aurl-cmd', + score: 0.0003680857021151614 + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'aurl-cmd', + score: 0.009652820108904094 + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'aurl-cmd', + score: 0.7504160124360846 + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'aurl-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'aurl-cmd', + score: 0.13586474005868793 + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'aurl-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'aurl-cmd', + score: 0.8973590434087177 + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'aurl-cmd', + score: 0.27111130260612365 + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'aurl-cmd', + score: 0.0038703587462843594 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'aurl-cmd', + score: 0.03741172773691362 + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'aurl-cmd', + score: 0.0004995635515943437 + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'aurl-cmd', + score: 7.847906405228455 + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'aurl-cmd', + score: 0.0174633138331273 + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'aurl-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'aurl-cmd', + score: 0.006776001543888959 + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5 + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'aurl-cmd', + score: 6.006262128895586e-5 + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'aurl-cmd', + score: 0.00017906650306643613 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'aurl-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'aurl-cmd', + score: 1.4380093454211778 + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'aurl-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'aurl-cmd', + score: 0.002233258780143355 + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'aurl-cmd', + score: 0.047007158741781095 + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'aurl-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'aurl-cmd', + score: 0.00018344383742255004 + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'aurl-cmd', + score: 0.0005446476945175932 + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'aurl-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'aurl-cmd', + score: 0.0018503938529945614 + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'aurl-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'aurl-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'aurl-cmd', + score: 0.0012546605780895737 + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'aurl-cmd', + score: 0.004515152477030062 + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'aurl-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'aurl-cmd', + score: 0.02445837629741638 + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'aurl-cmd', + score: 0.3311721696201715 + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'aurl-cmd', + score: 0.06967310843464661 + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'aurl-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'aurl-cmd', + score: 0.0012064581899162352 + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'aurl-cmd', + score: 0.9202908262245683 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'aurl-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'aurl-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'aurl-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411 + } + ], + bchart: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bchart-cmd', + score: 0.00037306820619479756 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bchart-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bchart-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bchart-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.015973401906548487 + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'bchart-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'bchart-cmd', + score: 0.0005981923692899367 + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.017834153815870245 + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'bchart-cmd', + score: 1.4595731795525781 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bchart-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'bchart-cmd', + score: 0.0055519509468004175 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bchart-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'bchart-cmd', + score: 0.09973951908678011 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bchart-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bchart-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'bchart-cmd', + score: 0.004649150613625593 + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'bchart-cmd', + score: 0.009331077109224957 + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'bchart-cmd', + score: 0.0012203054938872515 + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'bchart-cmd', + score: 0.0009170966832172938 + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'bchart-cmd', + score: 0.01590723355124104 + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'bchart-cmd', + score: 0.0018957469739775527 + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bchart-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bchart-cmd', + score: 0.004719094298848707 + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'bchart-cmd', + score: 0.0003209840085766927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bchart-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bchart-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.00926923425734719 + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'bchart-cmd', + score: 0.03654388342026623 + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'bchart-cmd', + score: 0.20852115286477566 + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'bchart-cmd', + score: 0.000264339771769041 + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.0014120076489723356 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bchart-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'bchart-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'bchart-cmd', + score: 0.0008147200475678891 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bchart-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.16906710888680052 + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.029302172361548254 + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'bchart-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'bchart-cmd', + score: 0.2864294797053033 + } + ], + pdftexcmds: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdftexcmds-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdftexcmds-cmd', + score: 0.002958865219480927 + } + ], + l3keys2e: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'l3keys2e-cmd', + score: 0.2864294797053033 + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'l3keys2e-cmd', + score: 0.2864294797053033 + } + ], + xfor: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xfor-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xfor-cmd', + score: 0.021170869458413965 + } + ], + accsupp: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'accsupp-cmd', + score: 0.00021116765384691477 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'accsupp-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'accsupp-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'accsupp-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'accsupp-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'accsupp-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'accsupp-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'accsupp-cmd', + score: 0.021170869458413965 + } + ], + trig: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'trig-cmd', + score: 0.008565354665444157 + } + ], + rerunfilecheck: [ + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'rerunfilecheck-cmd', + score: 0.010304996748556729 + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'rerunfilecheck-cmd', + score: 0.013774721817648336 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'rerunfilecheck-cmd', + score: 0.1789117552185788 + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'rerunfilecheck-cmd', + score: 0.006609629561859019 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rerunfilecheck-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rerunfilecheck-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'rerunfilecheck-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'rerunfilecheck-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'rerunfilecheck-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'rerunfilecheck-cmd', + score: 0.021170869458413965 + } + ], + pdfescape: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdfescape-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pdfescape-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdfescape-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdfescape-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfescape-cmd', + score: 0.008565354665444157 + } + ], + infwarerr: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'infwarerr-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'infwarerr-cmd', + score: 0.0058342578961340175 + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'infwarerr-cmd', + score: 0.023010789853665694 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'infwarerr-cmd', + score: 0.008565354665444157 + } + ], + kvsetkeys: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvsetkeys-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'kvsetkeys-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvsetkeys-cmd', + score: 0.008565354665444157 + } + ], + gettitlestring: [ + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'gettitlestring-cmd', + score: 0.07503475348393239 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'gettitlestring-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'gettitlestring-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gettitlestring-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'gettitlestring-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'gettitlestring-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gettitlestring-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gettitlestring-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'gettitlestring-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'gettitlestring-cmd', + score: 0.021170869458413965 + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'gettitlestring-cmd', + score: 0.021170869458413965 + } + ], + refcount: [ + { + caption: '\\thepage', + snippet: '\\thepage', + meta: 'refcount-cmd', + score: 0.0591555998103519 + } + ], + bitset: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bitset-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bitset-cmd', + score: 0.008565354665444157 + } + ], + etexcmds: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etexcmds-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'etexcmds-cmd', + score: 0.002958865219480927 + } + ], + intcalc: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'intcalc-cmd', + score: 0.002958865219480927 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'intcalc-cmd', + score: 0.008565354665444157 + } + ], + hycolor: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hycolor-cmd', + score: 0.00530510025314411 + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hycolor-cmd', + score: 0.008565354665444157 + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hycolor-cmd', + score: 0.00530510025314411 + } + ] +} diff --git a/services/web/app/src/Features/Newsletter/NewsletterManager.js b/services/web/app/src/Features/Newsletter/NewsletterManager.js new file mode 100644 index 0000000000..0ed879f0ac --- /dev/null +++ b/services/web/app/src/Features/Newsletter/NewsletterManager.js @@ -0,0 +1,182 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let mailchimp +const async = require('async') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') +const crypto = require('crypto') +const Mailchimp = require('mailchimp-api-v3') + +if ( + (Settings.mailchimp != null ? Settings.mailchimp.api_key : undefined) == null +) { + logger.info('Using newsletter provider: none') + mailchimp = { + request(opts, cb) { + return cb() + } + } +} else { + logger.info('Using newsletter provider: mailchimp') + mailchimp = new Mailchimp( + Settings.mailchimp != null ? Settings.mailchimp.api_key : undefined + ) +} + +module.exports = { + subscribe(user, callback) { + if (callback == null) { + callback = function() {} + } + const options = buildOptions(user, true) + logger.log( + { options, user, email: user.email }, + 'subscribing user to the mailing list' + ) + return mailchimp.request(options, function(err) { + if (err != null) { + logger.err({ err, user }, 'error subscribing person to newsletter') + } else { + logger.log({ user }, 'finished subscribing user to the newsletter') + } + return callback(err) + }) + }, + + unsubscribe(user, callback) { + if (callback == null) { + callback = function() {} + } + logger.log( + { user, email: user.email }, + 'trying to unsubscribe user to the mailing list' + ) + const options = buildOptions(user, false) + return mailchimp.request(options, function(err) { + if (err != null) { + logger.err({ err, user }, 'error unsubscribing person to newsletter') + } else { + logger.log({ user }, 'finished unsubscribing user to the newsletter') + } + return callback(err) + }) + }, + + changeEmail(oldEmail, newEmail, callback) { + if (callback == null) { + callback = function() {} + } + const options = buildOptions({ email: oldEmail }) + delete options.body.status + options.body.email_address = newEmail + logger.log({ oldEmail, newEmail, options }, 'changing email in newsletter') + return mailchimp.request(options, function(err) { + if ( + err != null && + __guard__(err != null ? err.message : undefined, x => + x.indexOf('merge fields were invalid') + ) !== -1 + ) { + logger.log( + { oldEmail, newEmail }, + 'unable to change email in newsletter, user has never subscribed' + ) + return callback() + } else if ( + err != null && + __guard__(err != null ? err.message : undefined, x1 => + x1.indexOf('could not be validated') + ) !== -1 + ) { + logger.log( + { oldEmail, newEmail }, + 'unable to change email in newsletter, user has previously unsubscribed or new email already exist on list' + ) + return callback() + } else if ( + err != null && + err.message.indexOf('is already a list member') !== -1 + ) { + logger.log( + { oldEmail, newEmail }, + 'unable to change email in newsletter, new email is already on mailing list' + ) + return callback() + } else if ( + err != null && + __guard__(err != null ? err.message : undefined, x2 => + x2.indexOf('looks fake or invalid') + ) !== -1 + ) { + logger.log( + { oldEmail, newEmail }, + 'unable to change email in newsletter, email looks fake to mailchimp' + ) + return callback() + } else if (err != null) { + logger.err( + { err, oldEmail, newEmail }, + 'error changing email in newsletter' + ) + return callback(err) + } else { + logger.log('finished changing email in the newsletter') + return callback() + } + }) + } +} + +const hashEmail = email => + crypto + .createHash('md5') + .update(email.toLowerCase()) + .digest('hex') + +var buildOptions = function(user, is_subscribed) { + const subscriber_hash = hashEmail(user.email) + const status = is_subscribed ? 'subscribed' : 'unsubscribed' + const opts = { + method: 'PUT', + path: `/lists/${ + Settings.mailchimp != null ? Settings.mailchimp.list_id : undefined + }/members/${subscriber_hash}`, + body: { + email_address: user.email, + status_if_new: status + } + } + + // only set status if we explictly want to set it + if (is_subscribed != null) { + opts.body.status = status + } + + if (user._id != null) { + opts.body.merge_fields = { + FNAME: user.first_name, + LNAME: user.last_name, + MONGO_ID: user._id + } + } + + return opts +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Notifications/NotificationsBuilder.js b/services/web/app/src/Features/Notifications/NotificationsBuilder.js new file mode 100644 index 0000000000..5dce932301 --- /dev/null +++ b/services/web/app/src/Features/Notifications/NotificationsBuilder.js @@ -0,0 +1,187 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const logger = require('logger-sharelatex') +const NotificationsHandler = require('./NotificationsHandler') +const request = require('request') +const settings = require('settings-sharelatex') + +module.exports = { + // Note: notification keys should be url-safe + + featuresUpgradedByAffiliation(affiliation, user) { + return { + key: `features-updated-by=${affiliation.institutionId}`, + create(callback) { + if (callback == null) { + callback = function() {} + } + const messageOpts = { institutionName: affiliation.institutionName } + return NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_features_upgraded_by_affiliation', + messageOpts, + null, + false, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function() {} + } + return NotificationsHandler.markAsRead(this.key, callback) + } + } + }, + + redundantPersonalSubscription(affiliation, user) { + return { + key: `redundant-personal-subscription-${affiliation.institutionId}`, + create(callback) { + if (callback == null) { + callback = function() {} + } + const messageOpts = { institutionName: affiliation.institutionName } + return NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_personal_subscription_not_required_due_to_affiliation', + messageOpts, + null, + false, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function() {} + } + return NotificationsHandler.markAsRead(this.key, callback) + } + } + }, + + projectInvite(invite, project, sendingUser, user) { + return { + key: `project-invite-${invite._id}`, + create(callback) { + if (callback == null) { + callback = function() {} + } + const messageOpts = { + userName: sendingUser.first_name, + projectName: project.name, + projectId: project._id.toString(), + token: invite.token + } + logger.log( + { + user_id: user._id, + project_id: project._id, + invite_id: invite._id, + key: this.key + }, + 'creating project invite notification for user' + ) + return NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_project_invite', + messageOpts, + invite.expires, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function() {} + } + return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + } + } + }, + + ipMatcherAffiliation(userId) { + return { + create(ip, callback) { + if (callback == null) { + callback = function() {} + } + if ( + !__guard__( + __guard__( + settings != null ? settings.apis : undefined, + x1 => x1.v1 + ), + x => x.url + ) + ) { + return null + } // service is not configured + return request( + { + method: 'GET', + url: `${settings.apis.v1.url}/api/v2/users/${userId}/ip_matcher`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + body: { ip }, + json: true, + timeout: 20 * 1000 + }, + function(error, response, body) { + if (error != null) { + return error + } + if (response.statusCode !== 200) { + return null + } + + const key = `ip-matched-affiliation-${body.id}` + const messageOpts = { + university_name: body.name, + content: body.enrolment_ad_html + } + logger.log( + { user_id: userId, key }, + 'creating notification key for user' + ) + return NotificationsHandler.createNotification( + userId, + key, + 'notification_ip_matched_affiliation', + messageOpts, + null, + false, + callback + ) + } + ) + }, + + read(university_id, callback) { + if (callback == null) { + callback = function() {} + } + const key = `ip-matched-affiliation-${university_id}` + return NotificationsHandler.markAsReadWithKey(userId, key, callback) + } + } + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Notifications/NotificationsController.js b/services/web/app/src/Features/Notifications/NotificationsController.js new file mode 100644 index 0000000000..a79dcc6d94 --- /dev/null +++ b/services/web/app/src/Features/Notifications/NotificationsController.js @@ -0,0 +1,42 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const NotificationsHandler = require('./NotificationsHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const logger = require('logger-sharelatex') +const _ = require('underscore') + +module.exports = { + getAllUnreadNotifications(req, res) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return NotificationsHandler.getUserNotifications(user_id, function( + err, + unreadNotifications + ) { + unreadNotifications = _.map(unreadNotifications, function(notification) { + notification.html = req.i18n.translate( + notification.templateKey, + notification.messageOpts + ) + return notification + }) + return res.send(unreadNotifications) + }) + }, + + markNotificationAsRead(req, res) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const { notification_id } = req.params + NotificationsHandler.markAsRead(user_id, notification_id, () => res.send()) + return logger.log({ user_id, notification_id }, 'mark notification as read') + } +} diff --git a/services/web/app/src/Features/Notifications/NotificationsHandler.js b/services/web/app/src/Features/Notifications/NotificationsHandler.js new file mode 100644 index 0000000000..0406069145 --- /dev/null +++ b/services/web/app/src/Features/Notifications/NotificationsHandler.js @@ -0,0 +1,152 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('settings-sharelatex') +const request = require('request') +const logger = require('logger-sharelatex') + +const oneSecond = 1000 + +const makeRequest = function(opts, callback) { + if ( + (settings.apis.notifications != null + ? settings.apis.notifications.url + : undefined) == null + ) { + return callback(null, { statusCode: 200 }) + } else { + return request(opts, callback) + } +} + +module.exports = { + getUserNotifications(user_id, callback) { + const opts = { + uri: `${ + settings.apis.notifications != null + ? settings.apis.notifications.url + : undefined + }/user/${user_id}`, + json: true, + timeout: oneSecond, + method: 'GET' + } + return makeRequest(opts, function(err, res, unreadNotifications) { + const statusCode = res != null ? res.statusCode : 500 + if (err != null || statusCode !== 200) { + const e = new Error( + `something went wrong getting notifications, ${err}, ${statusCode}` + ) + logger.err({ err }, 'something went wrong getting notifications') + return callback(null, []) + } else { + if (unreadNotifications == null) { + unreadNotifications = [] + } + return callback(null, unreadNotifications) + } + }) + }, + + createNotification( + user_id, + key, + templateKey, + messageOpts, + expiryDateTime, + forceCreate, + callback + ) { + if (!callback) { + callback = forceCreate + forceCreate = true + } + const payload = { + key, + messageOpts, + templateKey, + forceCreate + } + if (expiryDateTime != null) { + payload.expires = expiryDateTime + } + const opts = { + uri: `${ + settings.apis.notifications != null + ? settings.apis.notifications.url + : undefined + }/user/${user_id}`, + timeout: oneSecond, + method: 'POST', + json: payload + } + logger.log({ opts }, 'creating notification for user') + return makeRequest(opts, callback) + }, + + markAsReadWithKey(user_id, key, callback) { + const opts = { + uri: `${ + settings.apis.notifications != null + ? settings.apis.notifications.url + : undefined + }/user/${user_id}`, + method: 'DELETE', + timeout: oneSecond, + json: { + key + } + } + logger.log( + { user_id, key }, + 'sending mark notification as read with key to notifications api' + ) + return makeRequest(opts, callback) + }, + + markAsRead(user_id, notification_id, callback) { + const opts = { + method: 'DELETE', + uri: `${ + settings.apis.notifications != null + ? settings.apis.notifications.url + : undefined + }/user/${user_id}/notification/${notification_id}`, + timeout: oneSecond + } + logger.log( + { user_id, notification_id }, + 'sending mark notification as read to notifications api' + ) + return makeRequest(opts, callback) + }, + + // removes notification by key, without regard for user_id, + // should not be exposed to user via ui/router + markAsReadByKeyOnly(key, callback) { + const opts = { + uri: `${ + settings.apis.notifications != null + ? settings.apis.notifications.url + : undefined + }/key/${key}`, + method: 'DELETE', + timeout: oneSecond + } + logger.log( + { key }, + 'sending mark notification as read with key-only to notifications api' + ) + return makeRequest(opts, callback) + } +} diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetController.js b/services/web/app/src/Features/PasswordReset/PasswordResetController.js new file mode 100644 index 0000000000..306a8b9afe --- /dev/null +++ b/services/web/app/src/Features/PasswordReset/PasswordResetController.js @@ -0,0 +1,173 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const PasswordResetHandler = require('./PasswordResetHandler') +const RateLimiter = require('../../infrastructure/RateLimiter') +const AuthenticationController = require('../Authentication/AuthenticationController') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const UserGetter = require('../User/UserGetter') +const UserUpdater = require('../User/UserUpdater') +const UserSessionsManager = require('../User/UserSessionsManager') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') + +module.exports = { + renderRequestResetForm(req, res) { + logger.log('rendering request reset form') + return res.render('user/passwordReset', { title: 'reset_password' }) + }, + + requestReset(req, res) { + const email = req.body.email.trim().toLowerCase() + const opts = { + endpointName: 'password_reset_rate_limit', + timeInterval: 60, + subjectName: req.ip, + throttle: 6 + } + return RateLimiter.addCount(opts, function(err, canContinue) { + if (!canContinue) { + return res.send(429, { + message: req.i18n.translate('rate_limit_hit_wait') + }) + } + return PasswordResetHandler.generateAndEmailResetToken(email, function( + err, + status + ) { + if (err != null) { + return res.send(500, { + message: err != null ? err.message : undefined + }) + } else if (status === 'primary') { + return res.send(200, { + message: { text: req.i18n.translate('password_reset_email_sent') } + }) + } else if (status === 'secondary') { + return res.send(404, { + message: req.i18n.translate('secondary_email_password_reset') + }) + } else if (status === 'sharelatex') { + return res.send(404, { + message: `${req.i18n.translate('reset_from_sl')}` + }) + } else { + return res.send(404, { + message: req.i18n.translate('cant_find_email') + }) + } + }) + }) + }, + + renderSetPasswordForm(req, res) { + if (req.query.passwordResetToken != null) { + req.session.resetToken = req.query.passwordResetToken + return res.redirect('/user/password/set') + } + if (req.session.resetToken == null) { + return res.redirect('/user/password/reset') + } + return res.render('user/setPassword', { + title: 'set_password', + passwordResetToken: req.session.resetToken + }) + }, + + setNewUserPassword(req, res, next) { + const { passwordResetToken, password } = req.body + if ( + password == null || + password.length === 0 || + passwordResetToken == null || + passwordResetToken.length === 0 || + AuthenticationManager.validatePassword( + password != null ? password.trim() : undefined + ) != null + ) { + return res.sendStatus(400) + } + delete req.session.resetToken + return PasswordResetHandler.setNewUserPassword( + passwordResetToken != null ? passwordResetToken.trim() : undefined, + password != null ? password.trim() : undefined, + function(err, found, user_id) { + if (err && err.name && err.name === 'NotFoundError') { + return res.status(404).send('NotFoundError') + } else if (err && err.name && err.name === 'NotInV2Error') { + return res.status(403).send('NotInV2Error') + } else if (err && err.name && err.name === 'SLInV2Error') { + return res.status(403).send('SLInV2Error') + } else if (err && err.statusCode && err.statusCode === 500) { + return res.status(500) + } else if (err && !err.statusCode) { + return res.status(500) + } else if (found) { + if (user_id == null) { + return res.sendStatus(200) + } // will not exist for v1-only users + return UserSessionsManager.revokeAllUserSessions( + { _id: user_id }, + [], + function(err) { + if (err != null) { + return next(err) + } + return UserUpdater.removeReconfirmFlag(user_id, function(err) { + if (err != null) { + return next(err) + } + if (req.body.login_after) { + return UserGetter.getUser(user_id, { email: 1 }, function( + err, + user + ) { + if (err != null) { + return next(err) + } + return AuthenticationController.afterLoginSessionSetup( + req, + user, + function(err) { + if (err != null) { + logger.err( + { err, email: user.email }, + 'Error setting up session after setting password' + ) + return next(err) + } + return res.json({ + redir: + AuthenticationController._getRedirectFromSession( + req + ) || '/project' + }) + } + ) + }) + } else { + return res.sendStatus(200) + } + }) + } + ) + } else { + return res.sendStatus(404) + } + } + ) + } +} diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetHandler.js b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.js new file mode 100644 index 0000000000..4763899351 --- /dev/null +++ b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.js @@ -0,0 +1,179 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let PasswordResetHandler +const settings = require('settings-sharelatex') +const async = require('async') +const UserGetter = require('../User/UserGetter') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const EmailHandler = require('../Email/EmailHandler') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const logger = require('logger-sharelatex') +const V1Api = require('../V1/V1Api') + +module.exports = PasswordResetHandler = { + generateAndEmailResetToken(email, callback) { + if (callback == null) { + callback = function(error, status) {} + } + return PasswordResetHandler._getPasswordResetData(email, function( + error, + exists, + data + ) { + if (error != null) { + return callback(error, null) + } else if (exists) { + return OneTimeTokenHandler.getNewToken('password', data, function( + err, + token + ) { + if (err) { + return callback(err) + } + const emailOptions = { + to: email, + setNewPasswordUrl: `${ + settings.siteUrl + }/user/password/set?passwordResetToken=${token}&email=${encodeURIComponent( + email + )}` + } + return EmailHandler.sendEmail( + 'passwordResetRequested', + emailOptions, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, 'primary') + } + ) + }) + } else { + return UserGetter.getUserByAnyEmail(email, function(err, user) { + if (!user) { + return callback(error, null) + } else if ( + (user.overleaf != null ? user.overleaf.id : undefined) == null + ) { + return callback(error, 'sharelatex') + } else { + return callback(error, 'secondary') + } + }) + } + }) + }, + + setNewUserPassword(token, password, callback) { + if (callback == null) { + callback = function(error, found, user_id) {} + } + return OneTimeTokenHandler.getValueFromTokenAndExpire( + 'password', + token, + function(err, data) { + if (err) { + return callback(err) + } + if (data == null) { + return callback(null, false, null) + } + if (typeof data === 'string') { + // Backwards compatible with old format. + // Tokens expire after 1h, so this can be removed soon after deploy. + // Possibly we should keep this until we do an onsite release too. + data = { user_id: data } + } + if (data.user_id != null) { + return AuthenticationManager.setUserPassword( + data.user_id, + password, + function(err, reset) { + if (err) { + return callback(err) + } + return callback(null, reset, data.user_id) + } + ) + } else if (data.v1_user_id != null) { + return AuthenticationManager.setUserPasswordInV1( + data.v1_user_id, + password, + function(error, reset) { + if (error != null) { + return callback(error) + } + return UserGetter.getUser( + { 'overleaf.id': data.v1_user_id }, + { _id: 1 }, + function(error, user) { + if (error != null) { + return callback(error) + } + return callback( + null, + reset, + user != null ? user._id : undefined + ) + } + ) + } + ) + } + } + ) + }, + + _getPasswordResetData(email, callback) { + if (callback == null) { + callback = function(error, exists, data) {} + } + if (settings.overleaf != null) { + // Overleaf v2 + return V1Api.request( + { + url: '/api/v1/sharelatex/user_emails', + qs: { + email + }, + expectedStatusCodes: [404] + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + if (response.statusCode === 404) { + return callback(null, false) + } else { + return callback(null, true, { v1_user_id: body.user_id }) + } + } + ) + } else { + // ShareLaTeX + return UserGetter.getUserByMainEmail(email, function(err, user) { + if (err) { + return callback(err) + } + if (user == null || user.holdingAccount || user.overleaf != null) { + logger.err({ email }, 'user could not be found for password reset') + return callback(null, false) + } + return callback(null, true, { user_id: user._id }) + }) + } + } +} diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetRouter.js b/services/web/app/src/Features/PasswordReset/PasswordResetRouter.js new file mode 100644 index 0000000000..efdfb9b9c9 --- /dev/null +++ b/services/web/app/src/Features/PasswordReset/PasswordResetRouter.js @@ -0,0 +1,38 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const PasswordResetController = require('./PasswordResetController') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.get( + '/user/password/reset', + PasswordResetController.renderRequestResetForm + ) + webRouter.post('/user/password/reset', PasswordResetController.requestReset) + AuthenticationController.addEndpointToLoginWhitelist('/user/password/reset') + + webRouter.get( + '/user/password/set', + PasswordResetController.renderSetPasswordForm + ) + webRouter.post( + '/user/password/set', + PasswordResetController.setNewUserPassword + ) + AuthenticationController.addEndpointToLoginWhitelist('/user/password/set') + + return webRouter.post( + '/user/reconfirm', + PasswordResetController.requestReset + ) + } +} diff --git a/services/web/app/src/Features/Project/DocLinesComparitor.js b/services/web/app/src/Features/Project/DocLinesComparitor.js new file mode 100644 index 0000000000..ed0f0cf881 --- /dev/null +++ b/services/web/app/src/Features/Project/DocLinesComparitor.js @@ -0,0 +1,13 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +const _ = require('underscore') + +module.exports = { + areSame(lines1, lines2) { + if (!Array.isArray(lines1) || !Array.isArray(lines2)) { + return false + } + + return _.isEqual(lines1, lines2) + } +} diff --git a/services/web/app/src/Features/Project/ProjectApiController.js b/services/web/app/src/Features/Project/ProjectApiController.js new file mode 100644 index 0000000000..4985f074b2 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectApiController.js @@ -0,0 +1,30 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ProjectDetailsHandler = require('./ProjectDetailsHandler') +const logger = require('logger-sharelatex') + +module.exports = { + getProjectDetails(req, res, next) { + const { project_id } = req.params + return ProjectDetailsHandler.getDetails(project_id, function( + err, + projDetails + ) { + if (err != null) { + return next(err) + } + return res.json(projDetails) + }) + } +} diff --git a/services/web/app/src/Features/Project/ProjectCollabratecDetailsHandler.js b/services/web/app/src/Features/Project/ProjectCollabratecDetailsHandler.js new file mode 100644 index 0000000000..ef63ce994e --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectCollabratecDetailsHandler.js @@ -0,0 +1,166 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectCollabratecDetailsHandler +const { ObjectId } = require('mongojs') +const { Project } = require('../../models/Project') + +module.exports = ProjectCollabratecDetailsHandler = { + initializeCollabratecProject( + project_id, + user_id, + collabratec_document_id, + collabratec_privategroup_id, + callback + ) { + if (callback == null) { + callback = function(err) {} + } + return ProjectCollabratecDetailsHandler.setCollabratecUsers( + project_id, + [{ user_id, collabratec_document_id, collabratec_privategroup_id }], + callback + ) + }, + + isLinkedCollabratecUserProject(project_id, user_id, callback) { + if (callback == null) { + callback = function(err, isLinked) {} + } + try { + project_id = ObjectId(project_id) + user_id = ObjectId(user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { + _id: project_id, + collabratecUsers: { + $elemMatch: { + user_id + } + } + } + return Project.findOne(query, { _id: 1 }, function(err, project) { + if (err != null) { + callback(err) + } + return callback(null, project != null) + }) + }, + + linkCollabratecUserProject( + project_id, + user_id, + collabratec_document_id, + callback + ) { + if (callback == null) { + callback = function(err) {} + } + try { + project_id = ObjectId(project_id) + user_id = ObjectId(user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { + _id: project_id, + collabratecUsers: { + $not: { + $elemMatch: { + collabratec_document_id, + user_id + } + } + } + } + const update = { + $push: { + collabratecUsers: { + collabratec_document_id, + user_id + } + } + } + return Project.update(query, update, callback) + }, + + setCollabratecUsers(project_id, collabratec_users, callback) { + let err + if (callback == null) { + callback = function(err) {} + } + try { + project_id = ObjectId(project_id) + } catch (error) { + err = error + return callback(err) + } + if (!Array.isArray(collabratec_users)) { + callback(new Error('collabratec_users must be array')) + } + for (let collabratec_user of Array.from(collabratec_users)) { + try { + collabratec_user.user_id = ObjectId(collabratec_user.user_id) + } catch (error1) { + err = error1 + return callback(err) + } + } + const update = { $set: { collabratecUsers: collabratec_users } } + return Project.update({ _id: project_id }, update, callback) + }, + + unlinkCollabratecUserProject(project_id, user_id, callback) { + if (callback == null) { + callback = function(err) {} + } + try { + project_id = ObjectId(project_id) + user_id = ObjectId(user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { _id: project_id } + const update = { + $pull: { + collabratecUsers: { + user_id + } + } + } + return Project.update(query, update, callback) + }, + + updateCollabratecUserIds(old_user_id, new_user_id, callback) { + if (callback == null) { + callback = function(err) {} + } + try { + old_user_id = ObjectId(old_user_id) + new_user_id = ObjectId(new_user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { 'collabratecUsers.user_id': old_user_id } + const update = { $set: { 'collabratecUsers.$.user_id': new_user_id } } + const options = { multi: true } + return Project.update(query, update, options, callback) + } +} diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js new file mode 100644 index 0000000000..17f67de83e --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -0,0 +1,964 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-path-concat, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let generateThemeList, ProjectController +const async = require('async') +const logger = require('logger-sharelatex') +const projectDeleter = require('./ProjectDeleter') +const projectDuplicator = require('./ProjectDuplicator') +const projectCreationHandler = require('./ProjectCreationHandler') +const editorController = require('../Editor/EditorController') +const metrics = require('metrics-sharelatex') +const { User } = require('../../models/User') +const TagsHandler = require('../Tags/TagsHandler') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') +const NotificationsHandler = require('../Notifications/NotificationsHandler') +const LimitationsManager = require('../Subscription/LimitationsManager') +const underscore = require('underscore') +const Settings = require('settings-sharelatex') +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const fs = require('fs') +const InactiveProjectManager = require('../InactiveData/InactiveProjectManager') +const ProjectUpdateHandler = require('./ProjectUpdateHandler') +const ProjectGetter = require('./ProjectGetter') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const AuthenticationController = require('../Authentication/AuthenticationController') +const PackageVersions = require('../../infrastructure/PackageVersions') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const Sources = require('../Authorization/Sources') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const Modules = require('../../infrastructure/Modules') +const ProjectEntityHandler = require('./ProjectEntityHandler') +const UserGetter = require('../User/UserGetter') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const crypto = require('crypto') +const { V1ConnectionError } = require('../Errors/Errors') +const Features = require('../../infrastructure/Features') +const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandler') +const { getUserAffiliations } = require('../Institutions/InstitutionsAPI') +const V1Handler = require('../V1/V1Handler') + +module.exports = ProjectController = { + _isInPercentageRollout(rolloutName, objectId, percentage) { + if (Settings.bypassPercentageRollouts === true) { + return true + } + const data = `${rolloutName}:${objectId.toString()}` + const md5hash = crypto + .createHash('md5') + .update(data) + .digest('hex') + const counter = parseInt(md5hash.slice(26, 32), 16) + return counter % 100 < percentage + }, + + updateProjectSettings(req, res, next) { + const project_id = req.params.Project_id + + const jobs = [] + + if (req.body.compiler != null) { + jobs.push(callback => + editorController.setCompiler(project_id, req.body.compiler, callback) + ) + } + + if (req.body.imageName != null) { + jobs.push(callback => + editorController.setImageName(project_id, req.body.imageName, callback) + ) + } + + if (req.body.name != null) { + jobs.push(callback => + editorController.renameProject(project_id, req.body.name, callback) + ) + } + + if (req.body.spellCheckLanguage != null) { + jobs.push(callback => + editorController.setSpellCheckLanguage( + project_id, + req.body.spellCheckLanguage, + callback + ) + ) + } + + if (req.body.rootDocId != null) { + jobs.push(callback => + editorController.setRootDoc(project_id, req.body.rootDocId, callback) + ) + } + + return async.series(jobs, function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + }) + }, + + updateProjectAdminSettings(req, res, next) { + const project_id = req.params.Project_id + + const jobs = [] + if (req.body.publicAccessLevel != null) { + jobs.push(callback => + editorController.setPublicAccessLevel( + project_id, + req.body.publicAccessLevel, + callback + ) + ) + } + + return async.series(jobs, function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + }) + }, + + deleteProject(req, res) { + const project_id = req.params.Project_id + const forever = (req.query != null ? req.query.forever : undefined) != null + logger.log({ project_id, forever }, 'received request to archive project') + const user = AuthenticationController.getSessionUser(req) + const cb = function(err) { + if (err != null) { + return res.sendStatus(500) + } else { + return res.sendStatus(200) + } + } + + if (forever) { + return projectDeleter.deleteProject( + project_id, + { deleterUser: user, ipAddress: req.ip }, + cb + ) + } else { + return projectDeleter.archiveProject(project_id, cb) + } + }, + + restoreProject(req, res) { + const project_id = req.params.Project_id + logger.log({ project_id }, 'received request to restore project') + return projectDeleter.restoreProject(project_id, function(err) { + if (err != null) { + return res.sendStatus(500) + } else { + return res.sendStatus(200) + } + }) + }, + + cloneProject(req, res, next) { + res.setTimeout(5 * 60 * 1000) // allow extra time for the copy to complete + metrics.inc('cloned-project') + const project_id = req.params.Project_id + const { projectName } = req.body + logger.log({ project_id, projectName }, 'cloning project') + if (!AuthenticationController.isUserLoggedIn(req)) { + return res.send({ redir: '/register' }) + } + const currentUser = AuthenticationController.getSessionUser(req) + return projectDuplicator.duplicate( + currentUser, + project_id, + projectName, + function(err, project) { + if (err != null) { + logger.error( + { err, project_id, user_id: currentUser._id }, + 'error cloning project' + ) + return next(err) + } + return res.send({ + name: project.name, + project_id: project._id, + owner_ref: project.owner_ref + }) + } + ) + }, + + newProject(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const projectName = + req.body.projectName != null ? req.body.projectName.trim() : undefined + const { template } = req.body + logger.log( + { user: user_id, projectType: template, name: projectName }, + 'creating project' + ) + return async.waterfall( + [ + function(cb) { + if (template === 'example') { + return projectCreationHandler.createExampleProject( + user_id, + projectName, + cb + ) + } else { + return projectCreationHandler.createBasicProject( + user_id, + projectName, + cb + ) + } + } + ], + function(err, project) { + if (err != null) { + return next(err) + } + logger.log( + { project, user: user_id, name: projectName, templateType: template }, + 'created project' + ) + return res.send({ project_id: project._id }) + } + ) + }, + + renameProject(req, res, next) { + const project_id = req.params.Project_id + const newName = req.body.newProjectName + return editorController.renameProject(project_id, newName, function(err) { + if (err != null) { + return next(err) + } + return res.sendStatus(200) + }) + }, + + userProjectsJson(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return ProjectGetter.findAllUsersProjects( + user_id, + 'name lastUpdated publicAccesLevel archived owner_ref tokens', + function(err, projects) { + if (err != null) { + return next(err) + } + projects = ProjectController._buildProjectList(projects) + .filter(p => !p.archived) + .filter(p => !p.isV1Project) + .map(p => ({ _id: p.id, name: p.name, accessLevel: p.accessLevel })) + + return res.json({ projects }) + } + ) + }, + + projectEntitiesJson(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const project_id = req.params.Project_id + return ProjectGetter.getProject(project_id, function(err, project) { + if (err != null) { + return next(err) + } + return ProjectEntityHandler.getAllEntitiesFromProject(project, function( + err, + docs, + files + ) { + if (err != null) { + return next(err) + } + const entities = docs + .concat(files) + .sort((a, b) => a.path > b.path) // Sort by path ascending + .map(e => ({ + path: e.path, + type: e.doc != null ? 'doc' : 'file' + })) + return res.json({ project_id, entities }) + }) + }) + }, + + projectListPage(req, res, next) { + const timer = new metrics.Timer('project-list') + const user_id = AuthenticationController.getLoggedInUserId(req) + const currentUser = AuthenticationController.getSessionUser(req) + return async.parallel( + { + tags(cb) { + return TagsHandler.getAllTags(user_id, cb) + }, + notifications(cb) { + return NotificationsHandler.getUserNotifications(user_id, cb) + }, + projects(cb) { + return ProjectGetter.findAllUsersProjects( + user_id, + 'name lastUpdated lastUpdatedBy publicAccesLevel archived owner_ref tokens', + cb + ) + }, + v1Projects(cb) { + return Modules.hooks.fire('findAllV1Projects', user_id, function( + error, + projects + ) { + if (projects == null) { + projects = [] + } + if (error != null && error instanceof V1ConnectionError) { + return cb(null, { projects: [], tags: [], noConnection: true }) + } + return cb(error, projects[0]) + }) + }, // hooks.fire returns an array of results, only need first + hasSubscription(cb) { + return LimitationsManager.hasPaidSubscription(currentUser, function( + error, + hasPaidSubscription + ) { + if (error != null && error instanceof V1ConnectionError) { + return cb(null, true) + } + return cb(error, hasPaidSubscription) + }) + }, + user(cb) { + return User.findById( + user_id, + 'featureSwitches overleaf awareOfV2 features', + cb + ) + }, + userAffiliations(cb) { + return getUserAffiliations(user_id, cb) + } + }, + function(err, results) { + if (err != null) { + logger.err({ err }, 'error getting data for project list page') + return next(err) + } + logger.log({ results, user_id }, 'rendering project list') + const v1Tags = + (results.v1Projects != null ? results.v1Projects.tags : undefined) || + [] + const tags = results.tags[0].concat(v1Tags) + const notifications = require('underscore').map( + results.notifications, + function(notification) { + notification.html = req.i18n.translate( + notification.templateKey, + notification.messageOpts + ) + return notification + } + ) + const portalTemplates = ProjectController._buildPortalTemplatesList( + results.userAffiliations + ) + const projects = ProjectController._buildProjectList( + results.projects, + results.v1Projects != null ? results.v1Projects.projects : undefined + ) + const { user } = results + const { userAffiliations } = results + const warnings = ProjectController._buildWarningsList( + results.v1Projects + ) + + // in v2 add notifications for matching university IPs + if (Settings.overleaf != null) { + UserGetter.getUser(user_id, { lastLoginIp: 1 }, function( + error, + user + ) { + if (req.ip !== user.lastLoginIp) { + return NotificationsBuilder.ipMatcherAffiliation( + user._id, + req.ip + ).create() + } + }) + } + + return ProjectController._injectProjectUsers(projects, function( + error, + projects + ) { + if (error != null) { + return next(error) + } + const viewModel = { + title: 'your_projects', + priority_title: true, + projects, + tags, + notifications: notifications || [], + portalTemplates, + user, + userAffiliations, + hasSubscription: results.hasSubscription, + isShowingV1Projects: results.v1Projects != null, + warnings, + zipFileSizeLimit: Settings.maxUploadSize + } + + if ( + __guard__( + Settings != null ? Settings.algolia : undefined, + x => x.app_id + ) != null && + __guard__( + Settings != null ? Settings.algolia : undefined, + x1 => x1.read_only_api_key + ) != null + ) { + viewModel.showUserDetailsArea = true + viewModel.algolia_api_key = Settings.algolia.read_only_api_key + viewModel.algolia_app_id = Settings.algolia.app_id + } else { + viewModel.showUserDetailsArea = false + } + + const paidUser = + (user.features != null ? user.features.github : undefined) && + (user.features != null ? user.features.dropbox : undefined) // use a heuristic for paid account + const freeUserProportion = 0.1 + const sampleFreeUser = + parseInt(user._id.toString().slice(-2), 16) < + freeUserProportion * 255 + const showFrontWidget = paidUser || sampleFreeUser + logger.log( + { paidUser, sampleFreeUser, showFrontWidget }, + 'deciding whether to show front widget' + ) + if (showFrontWidget) { + viewModel.frontChatWidgetRoomId = + Settings.overleaf != null + ? Settings.overleaf.front_chat_widget_room_id + : undefined + } + + res.render('project/list', viewModel) + return timer.done() + }) + } + ) + }, + + loadEditor(req, res, next) { + let anonymous, user_id + const timer = new metrics.Timer('load-editor') + if (!Settings.editorIsOpen) { + return res.render('general/closed', { title: 'updating_site' }) + } + + if (AuthenticationController.isUserLoggedIn(req)) { + user_id = AuthenticationController.getLoggedInUserId(req) + anonymous = false + } else { + anonymous = true + user_id = null + } + + const project_id = req.params.Project_id + logger.log({ project_id, anonymous, user_id }, 'loading editor') + + // record failures to load the custom websocket + if ((req.query != null ? req.query.ws : undefined) === 'fallback') { + metrics.inc('load-editor-ws-fallback') + } + + return async.auto( + { + project(cb) { + return ProjectGetter.getProject( + project_id, + { + name: 1, + lastUpdated: 1, + track_changes: 1, + owner_ref: 1, + brandVariationId: 1, + overleaf: 1, + tokens: 1 + }, + function(err, project) { + if (err != null) { + return cb(err) + } + if ( + (project.overleaf != null ? project.overleaf.id : undefined) == + null || + (project.tokens != null + ? project.tokens.readAndWrite + : undefined) == null || + Settings.projectImportingCheckMaxCreateDelta == null + ) { + return cb(null, project) + } + const createDelta = + (new Date().getTime() - + new Date(project._id.getTimestamp()).getTime()) / + 1000 + if ( + !(createDelta < Settings.projectImportingCheckMaxCreateDelta) + ) { + return cb(null, project) + } + return V1Handler.getDocExported( + project.tokens.readAndWrite, + function(err, doc_exported) { + if (err != null) { + return next(err) + } + project.exporting = doc_exported.exporting + return cb(null, project) + } + ) + } + ) + }, + user(cb) { + if (user_id == null) { + return cb(null, defaultSettingsForAnonymousUser(user_id)) + } else { + return User.findById(user_id, function(err, user) { + logger.log({ project_id, user_id }, 'got user') + return cb(err, user) + }) + } + }, + subscription(cb) { + if (user_id == null) { + return cb() + } + return SubscriptionLocator.getUsersSubscription(user_id, cb) + }, + activate(cb) { + return InactiveProjectManager.reactivateProjectIfRequired( + project_id, + cb + ) + }, + markAsOpened(cb) { + // don't need to wait for this to complete + ProjectUpdateHandler.markAsOpened(project_id, function() {}) + return cb() + }, + isTokenMember(cb) { + cb = underscore.once(cb) + if (user_id == null) { + return cb() + } + return CollaboratorsHandler.userIsTokenMember(user_id, project_id, cb) + }, + brandVariation: [ + 'project', + function(cb, results) { + if ( + (results.project != null + ? results.project.brandVariationId + : undefined) == null + ) { + return cb() + } + return BrandVariationsHandler.getBrandVariationById( + results.project.brandVariationId, + (error, brandVariationDetails) => cb(error, brandVariationDetails) + ) + } + ] + }, + function(err, results) { + if (err != null) { + logger.err({ err }, 'error getting details for project page') + return next(err) + } + const { project } = results + const { user } = results + const { subscription } = results + const { brandVariation } = results + + const daysSinceLastUpdated = + (new Date() - project.lastUpdated) / 86400000 + logger.log( + { project_id, daysSinceLastUpdated }, + 'got db results for loading editor' + ) + + const token = TokenAccessHandler.getRequestToken(req, project_id) + const { isTokenMember } = results + return AuthorizationManager.getPrivilegeLevelForProject( + user_id, + project_id, + token, + function(error, privilegeLevel) { + let allowedFreeTrial + if (error != null) { + return next(error) + } + if ( + privilegeLevel == null || + privilegeLevel === PrivilegeLevels.NONE + ) { + return res.sendStatus(401) + } + + if (project.exporting) { + res.render('project/importing', { bodyClasses: ['editor'] }) + return + } + + if ( + subscription != null && + subscription.freeTrial != null && + subscription.freeTrial.expiresAt != null + ) { + allowedFreeTrial = !!subscription.freeTrial.allowed || true + } + + logger.log({ project_id }, 'rendering editor page') + res.render('project/editor', { + title: project.name, + priority_title: true, + bodyClasses: ['editor'], + project_id: project._id, + user: { + id: user_id, + email: user.email, + 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, + isAdmin: user.isAdmin + }, + userSettings: { + mode: user.ace.mode, + editorTheme: user.ace.theme, + fontSize: user.ace.fontSize, + autoComplete: user.ace.autoComplete, + autoPairDelimiters: user.ace.autoPairDelimiters, + pdfViewer: user.ace.pdfViewer, + syntaxValidation: user.ace.syntaxValidation, + fontFamily: user.ace.fontFamily, + lineHeight: user.ace.lineHeight, + overallTheme: user.ace.overallTheme + }, + trackChangesState: project.track_changes, + privilegeLevel, + chatUrl: Settings.apis.chat.url, + anonymous, + anonymousAccessToken: req._anonymousAccessToken, + isTokenMember, + languages: Settings.languages, + editorThemes: THEME_LIST, + maxDocLength: Settings.max_doc_length, + useV2History: !!__guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.display + ), + richTextTrackChangesEnabled: + (req.query != null ? req.query.rttc : undefined) === 'true' || + user.betaProgram, + showTestControls: + (req.query != null ? req.query.tc : undefined) === 'true' || + user.isAdmin, + brandVariation, + allowedImageNames: Settings.allowedImageNames || [], + gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl + }) + return timer.done() + } + ) + } + ) + }, + + _buildProjectList(allProjects, v1Projects) { + let project + if (v1Projects == null) { + v1Projects = [] + } + const { + owned, + readAndWrite, + readOnly, + tokenReadAndWrite, + tokenReadOnly + } = allProjects + const projects = [] + for (project of Array.from(owned)) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'owner', + Sources.OWNER + ) + ) + } + // Invite-access + for (project of Array.from(readAndWrite)) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readWrite', + Sources.INVITE + ) + ) + } + for (project of Array.from(readOnly)) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readOnly', + Sources.INVITE + ) + ) + } + for (project of Array.from(v1Projects)) { + projects.push(ProjectController._buildV1ProjectViewModel(project)) + } + // Token-access + // Only add these projects if they're not already present, this gives us cascading access + // from 'owner' => 'token-read-only' + for (project of Array.from(tokenReadAndWrite)) { + if ( + projects.filter(p => p.id.toString() === project._id.toString()) + .length === 0 + ) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readAndWrite', + Sources.TOKEN + ) + ) + } + } + for (project of Array.from(tokenReadOnly)) { + if ( + projects.filter(p => p.id.toString() === project._id.toString()) + .length === 0 + ) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readOnly', + Sources.TOKEN + ) + ) + } + } + + return projects + }, + + _buildProjectViewModel(project, accessLevel, source) { + TokenAccessHandler.protectTokens(project, accessLevel) + const model = { + id: project._id, + name: project.name, + lastUpdated: project.lastUpdated, + lastUpdatedBy: project.lastUpdatedBy, + publicAccessLevel: project.publicAccesLevel, + accessLevel, + source, + archived: !!project.archived, + owner_ref: project.owner_ref, + tokens: project.tokens, + isV1Project: false + } + return model + }, + + _buildV1ProjectViewModel(project) { + const projectViewModel = { + id: project.id, + name: project.title, + lastUpdated: new Date(project.updated_at * 1000), // Convert from epoch + archived: project.removed || project.archived, + isV1Project: true + } + if ( + (project.owner != null && project.owner.user_is_owner) || + (project.creator != null && project.creator.user_is_creator) + ) { + projectViewModel.accessLevel = 'owner' + } else { + projectViewModel.accessLevel = 'readOnly' + } + if (project.owner != null) { + projectViewModel.owner = { + first_name: project.owner.name + } + } else if (project.creator != null) { + projectViewModel.owner = { + first_name: project.creator.name + } + } + return projectViewModel + }, + + _injectProjectUsers(projects, callback) { + let project + if (callback == null) { + callback = function(error, projects) {} + } + const users = {} + for (project of Array.from(projects)) { + if (project.owner_ref != null) { + users[project.owner_ref.toString()] = true + } + if (project.lastUpdatedBy != null) { + users[project.lastUpdatedBy.toString()] = true + } + } + + const jobs = [] + for (let user_id in users) { + const _ = users[user_id] + ;(user_id => + jobs.push(callback => + UserGetter.getUserOrUserStubById( + user_id, + { first_name: 1, last_name: 1, email: 1 }, + function(error, user) { + if (error != null) { + return callback(error) + } + users[user_id] = user + return callback() + } + ) + ))(user_id) + } + + return async.series(jobs, function(error) { + for (project of Array.from(projects)) { + if (project.owner_ref != null) { + project.owner = users[project.owner_ref.toString()] + } + if (project.lastUpdatedBy != null) { + project.lastUpdatedBy = + users[project.lastUpdatedBy.toString()] || null + } + } + return callback(null, projects) + }) + }, + + _buildWarningsList(v1ProjectData) { + if (v1ProjectData == null) { + v1ProjectData = {} + } + const warnings = [] + if (v1ProjectData.noConnection) { + warnings.push('No V1 Connection') + } + if (v1ProjectData.hasHiddenV1Projects) { + warnings.push( + "Looks like you've got a lot of V1 projects! Some of them may be hidden on V2. To view them all, use the V1 dashboard." + ) + } + return warnings + }, + + _buildPortalTemplatesList(affiliations) { + if (affiliations == null) { + affiliations = [] + } + const portalTemplates = [] + for (let aff of Array.from(affiliations)) { + if ( + aff.portal && + aff.portal.slug && + aff.portal.templates_count && + aff.portal.templates_count > 0 + ) { + const portalPath = aff.institution.isUniversity ? '/edu/' : '/org/' + portalTemplates.push({ + name: aff.institution.name, + url: Settings.siteUrl + portalPath + aff.portal.slug + }) + } + } + return portalTemplates + } +} + +var defaultSettingsForAnonymousUser = user_id => ({ + id: user_id, + ace: { + mode: 'none', + theme: 'textmate', + fontSize: '12', + autoComplete: true, + spellCheckLanguage: '', + pdfViewer: '', + syntaxValidation: true + }, + subscription: { + freeTrial: { + allowed: true + } + }, + featureSwitches: { + github: false + } +}) + +var THEME_LIST = [] +;(generateThemeList = function() { + const files = fs.readdirSync( + __dirname + '/../../../../public/js/' + PackageVersions.lib('ace') + ) + return (() => { + const result = [] + for (let file of Array.from(files)) { + if (file.slice(-2) === 'js' && /^theme-/.test(file)) { + const cleanName = file.slice(0, -3).slice(6) + result.push(THEME_LIST.push(cleanName)) + } else { + result.push(undefined) + } + } + return result + })() +})() + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Project/ProjectCreationHandler.js b/services/web/app/src/Features/Project/ProjectCreationHandler.js new file mode 100644 index 0000000000..306e48dd8a --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectCreationHandler.js @@ -0,0 +1,343 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-path-concat, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectCreationHandler +const logger = require('logger-sharelatex') +const async = require('async') +const metrics = require('metrics-sharelatex') +const Settings = require('settings-sharelatex') +const { ObjectId } = require('mongoose').Types +const { Project } = require('../../models/Project') +const { Folder } = require('../../models/Folder') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') +const ProjectDetailsHandler = require('./ProjectDetailsHandler') +const HistoryManager = require('../History/HistoryManager') +const { User } = require('../../models/User') +const fs = require('fs') +const Path = require('path') +const _ = require('underscore') +const AnalyticsManger = require('../Analytics/AnalyticsManager') + +module.exports = ProjectCreationHandler = { + createBlankProject(owner_id, projectName, attributes, callback) { + if (callback == null) { + callback = function(error, project) {} + } + metrics.inc('project-creation') + if (arguments.length === 3) { + callback = attributes + attributes = null + } + + return ProjectDetailsHandler.validateProjectName(projectName, function( + error + ) { + if (error != null) { + return callback(error) + } + logger.log({ owner_id, projectName }, 'creating blank project') + if (attributes != null) { + return ProjectCreationHandler._createBlankProject( + owner_id, + projectName, + attributes, + function(error, project) { + if (error != null) { + return callback(error) + } + AnalyticsManger.recordEvent(owner_id, 'project-imported', { + projectId: project._id, + attributes + }) + return callback(error, project) + } + ) + } else { + return HistoryManager.initializeProject(function(error, history) { + if (error != null) { + return callback(error) + } + attributes = { + overleaf: { + history: { id: history != null ? history.overleaf_id : undefined } + } + } + return ProjectCreationHandler._createBlankProject( + owner_id, + projectName, + attributes, + function(error, project) { + if (error != null) { + return callback(error) + } + AnalyticsManger.recordEvent(owner_id, 'project-created', { + projectId: project._id + }) + return callback(error, project) + } + ) + }) + } + }) + }, + + _createBlankProject(owner_id, projectName, attributes, callback) { + if (callback == null) { + callback = function(error, project) {} + } + const rootFolder = new Folder({ name: 'rootFolder' }) + + attributes.owner_ref = new ObjectId(owner_id) + attributes.name = projectName + const project = new Project(attributes) + + Object.assign(project, attributes) + + if ( + __guard__( + Settings.apis != null ? Settings.apis.project_history : undefined, + x => x.displayHistoryForNewProjects + ) + ) { + project.overleaf.history.display = true + } + if (Settings.currentImageName != null) { + // avoid clobbering any imageName already set in attributes (e.g. importedImageName) + if (project.imageName == null) { + project.imageName = Settings.currentImageName + } + } + project.rootFolder[0] = rootFolder + return User.findById(owner_id, 'ace.spellCheckLanguage', function( + err, + user + ) { + if (user != null) { + // It's possible the owner_id is a UserStub + project.spellCheckLanguage = user.ace.spellCheckLanguage + } + return project.save(function(err) { + if (err != null) { + return callback(err) + } + return callback(err, project) + }) + }) + }, + + createProjectFromSnippet(owner_id, projectName, docLines, callback) { + if (callback == null) { + callback = function(error, project) {} + } + return this.createBlankProject(owner_id, projectName, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + return ProjectCreationHandler._createRootDoc( + project, + owner_id, + docLines, + callback + ) + }) + }, + + createBasicProject(owner_id, projectName, callback) { + if (callback == null) { + callback = function(error, project) {} + } + const self = this + return this.createBlankProject(owner_id, projectName, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + return self._buildTemplate( + 'mainbasic.tex', + owner_id, + projectName, + function(error, docLines) { + if (error != null) { + return callback(error) + } + return ProjectCreationHandler._createRootDoc( + project, + owner_id, + docLines, + callback + ) + } + ) + }) + }, + + createExampleProject(owner_id, projectName, callback) { + if (callback == null) { + callback = function(error, project) {} + } + const self = this + return this.createBlankProject(owner_id, projectName, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + return async.series( + [ + callback => + self._buildTemplate('main.tex', owner_id, projectName, function( + error, + docLines + ) { + if (error != null) { + return callback(error) + } + return ProjectCreationHandler._createRootDoc( + project, + owner_id, + docLines, + callback + ) + }), + callback => + self._buildTemplate( + 'references.bib', + owner_id, + projectName, + function(error, docLines) { + if (error != null) { + return callback(error) + } + return ProjectEntityUpdateHandler.addDoc( + project._id, + project.rootFolder[0]._id, + 'references.bib', + docLines, + owner_id, + (error, doc) => callback(error) + ) + } + ), + function(callback) { + const universePath = Path.resolve( + __dirname + '/../../../templates/project_files/universe.jpg' + ) + return ProjectEntityUpdateHandler.addFile( + project._id, + project.rootFolder[0]._id, + 'universe.jpg', + universePath, + null, + owner_id, + callback + ) + } + ], + error => callback(error, project) + ) + }) + }, + + _createRootDoc(project, owner_id, docLines, callback) { + if (callback == null) { + callback = function(error, project) {} + } + return ProjectEntityUpdateHandler.addDoc( + project._id, + project.rootFolder[0]._id, + 'main.tex', + docLines, + owner_id, + function(error, doc) { + if (error != null) { + logger.err( + { err: error }, + 'error adding root doc when creating project' + ) + return callback(error) + } + return ProjectEntityUpdateHandler.setRootDoc( + project._id, + doc._id, + error => callback(error, project) + ) + } + ) + }, + + _buildTemplate(template_name, user_id, project_name, callback) { + if (callback == null) { + callback = function(error, output) {} + } + return User.findById(user_id, 'first_name last_name', function( + error, + user + ) { + if (error != null) { + return callback(error) + } + const monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ] + + const templatePath = Path.resolve( + __dirname + `/../../../templates/project_files/${template_name}` + ) + return fs.readFile(templatePath, function(error, template) { + if (error != null) { + return callback(error) + } + const data = { + project_name, + user, + year: new Date().getUTCFullYear(), + month: monthNames[new Date().getUTCMonth()] + } + const output = _.template(template.toString(), data) + return callback(null, output.split('\n')) + }) + }) + } +} + +metrics.timeAsyncMethod( + ProjectCreationHandler, + 'createBlankProject', + 'mongo.ProjectCreationHandler', + logger +) + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Project/ProjectDeleter.js b/services/web/app/src/Features/Project/ProjectDeleter.js new file mode 100644 index 0000000000..606ffb1b08 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectDeleter.js @@ -0,0 +1,191 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectDeleter +const { Project } = require('../../models/Project') +const { DeletedProject } = require('../../models/DeletedProject') +const logger = require('logger-sharelatex') +const documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const tagsHandler = require('../Tags/TagsHandler') +const async = require('async') +const FileStoreHandler = require('../FileStore/FileStoreHandler') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') + +module.exports = ProjectDeleter = { + markAsDeletedByExternalSource(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { project_id }, + 'marking project as deleted by external data source' + ) + const conditions = { _id: project_id } + const update = { deletedByExternalDataSource: true } + + return Project.update(conditions, update, {}, err => + require('../Editor/EditorController').notifyUsersProjectHasBeenDeletedOrRenamed( + project_id, + () => callback() + ) + ) + }, + + unmarkAsDeletedByExternalSource(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { project_id }, + 'removing flag marking project as deleted by external data source' + ) + const conditions = { _id: project_id.toString() } + const update = { deletedByExternalDataSource: false } + return Project.update(conditions, update, {}, callback) + }, + + deleteUsersProjects(user_id, callback) { + logger.log({ user_id }, 'deleting users projects') + + return Project.find({ owner_ref: user_id }, function(error, projects) { + if (error != null) { + return callback(error) + } + return async.each( + projects, + (project, cb) => ProjectDeleter.deleteProject(project._id, cb), + function(err) { + if (err != null) { + return callback(err) + } + return CollaboratorsHandler.removeUserFromAllProjets( + user_id, + callback + ) + } + ) + }) + }, + + deleteProject(project_id, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function(error) {} + } + const data = {} + logger.log({ project_id }, 'deleting project') + + if (typeof options === 'function') { + callback = options + options = {} + } + + return async.waterfall( + [ + cb => + Project.findOne({ _id: project_id }, (err, project) => + cb(err, project) + ), + function(project, cb) { + const deletedProject = new DeletedProject() + deletedProject.project = project + deletedProject.deleterData = { + deletedAt: new Date(), + deleterId: + options.deleterUser != null ? options.deleterUser._id : undefined, + deleterIpAddress: options.ipAddress + } + + if (project == null) { + return callback(new Errors.NotFoundError('project not found')) + } + + return deletedProject.save(err => cb(err, deletedProject)) + }, + (deletedProject, cb) => + documentUpdaterHandler.flushProjectToMongoAndDelete(project_id, err => + cb(err, deletedProject) + ), + function(deletedProject, cb) { + CollaboratorsHandler.getMemberIds(project_id, function( + error, + member_ids + ) { + if (member_ids == null) { + member_ids = [] + } + return Array.from(member_ids).map(member_id => + tagsHandler.removeProjectFromAllTags( + member_id, + project_id, + function(err) {} + ) + ) + }) + return cb(null, deletedProject) + }, // doesn't matter if this fails or the order it happens in + (deletedProject, cb) => + Project.remove({ _id: project_id }, err => cb(err, deletedProject)) + ], + function(err, deletedProject) { + if (err != null) { + logger.err({ err }, 'problem deleting project') + return callback(err) + } + logger.log( + { project_id }, + 'successfully deleting project from user request' + ) + return callback(null, deletedProject) + } + ) + }, + + archiveProject(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id }, 'archived project from user request') + return Project.update( + { _id: project_id }, + { $set: { archived: true } }, + function(err) { + if (err != null) { + logger.err({ err }, 'problem archived project') + return callback(err) + } + logger.log( + { project_id }, + 'successfully archived project from user request' + ) + return callback() + } + ) + }, + + restoreProject(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return Project.update( + { _id: project_id }, + { $unset: { archived: true } }, + callback + ) + } +} diff --git a/services/web/app/src/Features/Project/ProjectDetailsHandler.js b/services/web/app/src/Features/Project/ProjectDetailsHandler.js new file mode 100644 index 0000000000..54e646432b --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectDetailsHandler.js @@ -0,0 +1,429 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectDetailsHandler +const ProjectGetter = require('./ProjectGetter') +const UserGetter = require('../User/UserGetter') +const { Project } = require('../../models/Project') +const { ObjectId } = require('mongojs') +const logger = require('logger-sharelatex') +const tpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +const _ = require('underscore') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const Errors = require('../Errors/Errors') +const ProjectTokenGenerator = require('./ProjectTokenGenerator') +const ProjectEntityHandler = require('./ProjectEntityHandler') +const ProjectHelper = require('./ProjectHelper') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const settings = require('settings-sharelatex') + +module.exports = ProjectDetailsHandler = { + getDetails(project_id, callback) { + return ProjectGetter.getProject( + project_id, + { + name: true, + description: true, + compiler: true, + features: true, + owner_ref: true, + overleaf: true + }, + function(err, project) { + if (err != null) { + logger.err({ err, project_id }, 'error getting project') + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('project not found')) + } + return UserGetter.getUser(project.owner_ref, function(err, user) { + if (err != null) { + return callback(err) + } + const details = { + name: project.name, + description: project.description, + compiler: project.compiler, + features: + (user != null ? user.features : undefined) || + settings.defaultFeatures + } + + if (project.overleaf != null) { + details.overleaf = project.overleaf + } + + logger.log({ project_id, details }, 'getting project details') + return callback(err, details) + }) + } + ) + }, + + getProjectDescription(project_id, callback) { + return ProjectGetter.getProject( + project_id, + { description: true }, + (err, project) => + callback(err, project != null ? project.description : undefined) + ) + }, + + setProjectDescription(project_id, description, callback) { + const conditions = { _id: project_id } + const update = { description } + logger.log( + { conditions, update, project_id, description }, + 'setting project description' + ) + return Project.update(conditions, update, function(err) { + if (err != null) { + logger.err({ err }, 'something went wrong setting project description') + } + return callback(err) + }) + }, + + transferOwnership(project_id, user_id, suffix, callback) { + if (suffix == null) { + suffix = '' + } + if (typeof suffix === 'function') { + callback = suffix + suffix = '' + } + return ProjectGetter.getProject( + project_id, + { owner_ref: true, name: true }, + function(err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('project not found')) + } + if (project.owner_ref === user_id) { + return callback() + } + + return UserGetter.getUser(user_id, function(err, user) { + if (err != null) { + return callback(err) + } + if (user == null) { + return callback(new Errors.NotFoundError('user not found')) + } + + // we make sure the user to which the project is transferred is not a collaborator for the project, + // this prevents any conflict during unique name generation + return CollaboratorsHandler.removeUserFromProject( + project_id, + user_id, + function(err) { + if (err != null) { + return callback(err) + } + return ProjectDetailsHandler.generateUniqueName( + user_id, + project.name + suffix, + function(err, name) { + if (err != null) { + return callback(err) + } + return Project.update( + { _id: project_id }, + { + $set: { + owner_ref: user_id, + name + } + }, + function(err) { + if (err != null) { + return callback(err) + } + return ProjectEntityHandler.flushProjectToThirdPartyDataStore( + project_id, + callback + ) + } + ) + } + ) + } + ) + }) + } + ) + }, + + renameProject(project_id, newName, callback) { + if (callback == null) { + callback = function() {} + } + return ProjectDetailsHandler.validateProjectName(newName, function(error) { + if (error != null) { + return callback(error) + } + logger.log({ project_id, newName }, 'renaming project') + return ProjectGetter.getProject(project_id, { name: true }, function( + err, + project + ) { + if (err != null || project == null) { + logger.err( + { err, project_id }, + 'error getting project or could not find it todo project rename' + ) + return callback(err) + } + const oldProjectName = project.name + return Project.update( + { _id: project_id }, + { name: newName }, + (err, project) => { + if (err != null) { + return callback(err) + } + return tpdsUpdateSender.moveEntity( + { + project_id, + project_name: oldProjectName, + newProjectName: newName + }, + callback + ) + } + ) + }) + }) + }, + + MAX_PROJECT_NAME_LENGTH: 150, + validateProjectName(name, callback) { + if (callback == null) { + callback = function(error) {} + } + if (name == null || name.length === 0) { + return callback( + new Errors.InvalidNameError('Project name cannot be blank') + ) + } else if (name.length > this.MAX_PROJECT_NAME_LENGTH) { + return callback(new Errors.InvalidNameError('Project name is too long')) + } else if (name.indexOf('/') > -1) { + return callback( + new Errors.InvalidNameError('Project name cannot contain / characters') + ) + } else if (name.indexOf('\\') > -1) { + return callback( + new Errors.InvalidNameError('Project name cannot contain \\ characters') + ) + } else { + return callback() + } + }, + + generateUniqueName(user_id, name, suffixes, callback) { + if (suffixes == null) { + suffixes = [] + } + if (callback == null) { + callback = function(error, newName) {} + } + if (arguments.length === 3 && typeof suffixes === 'function') { + // make suffixes an optional argument + callback = suffixes + suffixes = [] + } + return ProjectDetailsHandler.ensureProjectNameIsUnique( + user_id, + name, + suffixes, + callback + ) + }, + + // FIXME: we should put a lock around this to make it completely safe, but we would need to do that at + // the point of project creation, rather than just checking the name at the start of the import. + // If we later move this check into ProjectCreationHandler we can ensure all new projects are created + // with a unique name. But that requires thinking through how we would handle incoming projects from + // dropbox for example. + ensureProjectNameIsUnique(user_id, name, suffixes, callback) { + if (suffixes == null) { + suffixes = [] + } + if (callback == null) { + callback = function(error, name, changed) {} + } + return ProjectGetter.findAllUsersProjects(user_id, { name: 1 }, function( + error, + allUsersProjectNames + ) { + if (error != null) { + return callback(error) + } + // allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]} + // collect all of the names and flatten them into a single array + const projectNameList = _.pluck( + _.flatten(_.values(allUsersProjectNames)), + 'name' + ) + return ProjectHelper.ensureNameIsUnique( + projectNameList, + name, + suffixes, + ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH, + callback + ) + }) + }, + + fixProjectName(name) { + if (name === '' || !name) { + name = 'Untitled' + } + if (name.indexOf('/') > -1) { + // v2 does not allow / in a project name + name = name.replace(/\//g, '-') + } + if (name.indexOf('\\') > -1) { + // backslashes in project name will prevent syncing to dropbox + name = name.replace(/\\/g, '') + } + if (name.length > this.MAX_PROJECT_NAME_LENGTH) { + name = name.substr(0, this.MAX_PROJECT_NAME_LENGTH) + } + return name + }, + + setPublicAccessLevel(project_id, newAccessLevel, callback) { + if (callback == null) { + callback = function() {} + } + logger.log({ project_id, level: newAccessLevel }, 'set public access level') + // DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer + // be passed here. Remove after token-based access has been live for a while + if ( + project_id != null && + newAccessLevel != null && + _.include( + [ + PublicAccessLevels.READ_ONLY, + PublicAccessLevels.READ_AND_WRITE, + PublicAccessLevels.PRIVATE, + PublicAccessLevels.TOKEN_BASED + ], + newAccessLevel + ) + ) { + return Project.update( + { _id: project_id }, + { publicAccesLevel: newAccessLevel }, + err => callback(err) + ) + } + }, + + ensureTokensArePresent(project_id, callback) { + if (callback == null) { + callback = function(err, tokens) {} + } + return ProjectGetter.getProject(project_id, { tokens: 1 }, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if ( + project.tokens != null && + project.tokens.readOnly != null && + project.tokens.readAndWrite != null + ) { + logger.log({ project_id }, 'project already has tokens') + return callback(null, project.tokens) + } else { + logger.log( + { + project_id, + has_tokens: project.tokens != null, + has_readOnly: + __guard__( + project != null ? project.tokens : undefined, + x => x.readOnly + ) != null, + has_readAndWrite: + __guard__( + project != null ? project.tokens : undefined, + x1 => x1.readAndWrite + ) != null + }, + 'generating tokens for project' + ) + return ProjectDetailsHandler._generateTokens(project, function(err) { + if (err != null) { + return callback(err) + } + return Project.update( + { _id: project_id }, + { $set: { tokens: project.tokens } }, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, project.tokens) + } + ) + }) + } + }) + }, + + _generateTokens(project, callback) { + if (callback == null) { + callback = function(err) {} + } + if (!project.tokens) { + project.tokens = {} + } + const { tokens } = project + if (tokens.readAndWrite == null) { + const { token, numericPrefix } = ProjectTokenGenerator.readAndWriteToken() + tokens.readAndWrite = token + tokens.readAndWritePrefix = numericPrefix + } + if (tokens.readOnly == null) { + return ProjectTokenGenerator.generateUniqueReadOnlyToken(function( + err, + token + ) { + if (err != null) { + return callback(err) + } + tokens.readOnly = token + return callback() + }) + } else { + return callback() + } + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Project/ProjectDuplicator.js b/services/web/app/src/Features/Project/ProjectDuplicator.js new file mode 100644 index 0000000000..ffa088f841 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectDuplicator.js @@ -0,0 +1,312 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectDuplicator +const projectCreationHandler = require('./ProjectCreationHandler') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') +const projectLocator = require('./ProjectLocator') +const projectOptionsHandler = require('./ProjectOptionsHandler') +const projectDeleter = require('./ProjectDeleter') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const DocstoreManager = require('../Docstore/DocstoreManager') +const ProjectGetter = require('./ProjectGetter') +const _ = require('underscore') +const async = require('async') +const logger = require('logger-sharelatex') + +module.exports = ProjectDuplicator = { + _copyDocs( + owner_id, + newProject, + originalRootDoc, + originalFolder, + desFolder, + docContents, + callback + ) { + const setRootDoc = _.once(doc_id => + ProjectEntityUpdateHandler.setRootDoc(newProject._id, doc_id) + ) + const docs = originalFolder.docs || [] + const jobs = docs.map( + doc => + function(cb) { + if ((doc != null ? doc._id : undefined) == null) { + return callback() + } + const content = docContents[doc._id.toString()] + return ProjectEntityUpdateHandler.addDoc( + newProject._id, + desFolder._id, + doc.name, + content.lines, + owner_id, + function(err, newDoc) { + if (err != null) { + logger.err({ err }, 'error copying doc') + return callback(err) + } + if ( + originalRootDoc != null && + newDoc.name === originalRootDoc.name + ) { + setRootDoc(newDoc._id) + } + return cb() + } + ) + } + ) + + return async.series(jobs, callback) + }, + + _copyFiles( + owner_id, + newProject, + originalProject_id, + originalFolder, + desFolder, + callback + ) { + const fileRefs = originalFolder.fileRefs || [] + let firstError = null // track first error to exit gracefully from parallel copy + const jobs = fileRefs.map( + file => + function(cb) { + if (firstError != null) { + return async.setImmediate(cb) + } // skip further copies if an error has occurred + return ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject( + newProject._id, + newProject, + desFolder._id, + originalProject_id, + file, + owner_id, + function(err) { + if (err != null) { + if (!firstError) { + firstError = err + } + } // set the error flag if this copy failed + return cb() + } + ) + } + ) + // If one of these jobs fails then we wait until all running jobs have + // finished, skipping those which have not started yet. We need to wait + // for all the copy jobs to finish to avoid them writing to the project + // entry in the background while we are deleting it. + return async.parallelLimit(jobs, 5, function(err) { + if (firstError != null) { + return callback(firstError) + } + if (err != null) { + return callback(err) + } // shouldn't happen + return callback() + }) + }, + + _copyFolderRecursivly( + owner_id, + newProject_id, + originalProject_id, + originalRootDoc, + originalFolder, + desFolder, + docContents, + callback + ) { + return ProjectGetter.getProject( + newProject_id, + { rootFolder: true, name: true }, + function(err, newProject) { + if (err != null) { + logger.err({ project_id: newProject_id }, 'could not get project') + return callback(err) + } + + const folders = originalFolder.folders || [] + + const jobs = folders.map( + childFolder => + function(cb) { + if ((childFolder != null ? childFolder._id : undefined) == null) { + return cb() + } + return ProjectEntityUpdateHandler.addFolder( + newProject._id, + desFolder != null ? desFolder._id : undefined, + childFolder.name, + function(err, newFolder) { + if (err != null) { + return cb(err) + } + return ProjectDuplicator._copyFolderRecursivly( + owner_id, + newProject_id, + originalProject_id, + originalRootDoc, + childFolder, + newFolder, + docContents, + cb + ) + } + ) + } + ) + + jobs.push(cb => + ProjectDuplicator._copyFiles( + owner_id, + newProject, + originalProject_id, + originalFolder, + desFolder, + cb + ) + ) + jobs.push(cb => + ProjectDuplicator._copyDocs( + owner_id, + newProject, + originalRootDoc, + originalFolder, + desFolder, + docContents, + cb + ) + ) + + return async.series(jobs, callback) + } + ) + }, + + duplicate(owner, originalProject_id, newProjectName, callback) { + const jobs = { + flush(cb) { + return DocumentUpdaterHandler.flushProjectToMongo( + originalProject_id, + cb + ) + }, + originalProject(cb) { + return ProjectGetter.getProject( + originalProject_id, + { compiler: true, rootFolder: true, rootDoc_id: true }, + cb + ) + }, + originalRootDoc(cb) { + return projectLocator.findRootDoc( + { project_id: originalProject_id }, + cb + ) + }, + docContentsArray(cb) { + return DocstoreManager.getAllDocs(originalProject_id, cb) + } + } + + // Get the contents of the original project first + return async.series(jobs, function(err, results) { + if (err != null) { + logger.err( + { err, originalProject_id }, + 'error duplicating project reading original project' + ) + return callback(err) + } + let { originalProject, originalRootDoc, docContentsArray } = results + + originalRootDoc = originalRootDoc != null ? originalRootDoc[0] : undefined + + const docContents = {} + for (let docContent of Array.from(docContentsArray)) { + docContents[docContent._id] = docContent + } + + // Now create the new project, cleaning it up on failure if necessary + return projectCreationHandler.createBlankProject( + owner._id, + newProjectName, + function(err, newProject) { + if (err != null) { + logger.err( + { err, originalProject_id }, + 'error duplicating project when creating new project' + ) + return callback(err) + } + + const copyJobs = { + setCompiler(cb) { + return projectOptionsHandler.setCompiler( + newProject._id, + originalProject.compiler, + cb + ) + }, + copyFiles(cb) { + return ProjectDuplicator._copyFolderRecursivly( + owner._id, + newProject._id, + originalProject_id, + originalRootDoc, + originalProject.rootFolder[0], + newProject.rootFolder[0], + docContents, + cb + ) + } + } + + // Copy the contents of the original project into the new project + return async.series(copyJobs, function(err) { + if (err != null) { + logger.err( + { + err, + originalProject_id, + newProjectName, + newProject_id: newProject._id + }, + 'error cloning project, will delete broken clone' + ) + // Clean up broken clone on error. + // Make sure we delete the new failed project, not the original one! + return projectDeleter.deleteProject(newProject._id, function( + delete_err + ) { + if (delete_err != null) { + logger.error( + { newProject_id: newProject._id, delete_err }, + 'error deleting broken clone of project' + ) + } + return callback(err) + }) + } else { + return callback(null, newProject) + } + }) + } + ) + }) + } +} diff --git a/services/web/app/src/Features/Project/ProjectEditorHandler.js b/services/web/app/src/Features/Project/ProjectEditorHandler.js new file mode 100644 index 0000000000..ed23ebdb8e --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEditorHandler.js @@ -0,0 +1,145 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectEditorHandler +const _ = require('underscore') +const Path = require('path') + +module.exports = ProjectEditorHandler = { + trackChangesAvailable: false, + + buildProjectModelView(project, members, invites) { + let owner, ownerFeatures + const result = { + _id: project._id, + name: project.name, + rootDoc_id: project.rootDoc_id, + rootFolder: [this.buildFolderModelView(project.rootFolder[0])], + publicAccesLevel: project.publicAccesLevel, + dropboxEnabled: !!project.existsInDropbox, + compiler: project.compiler, + description: project.description, + spellCheckLanguage: project.spellCheckLanguage, + deletedByExternalDataSource: project.deletedByExternalDataSource || false, + deletedDocs: project.deletedDocs, + members: [], + invites, + tokens: project.tokens, + imageName: + project.imageName != null ? Path.basename(project.imageName) : undefined + } + + if (result.invites == null) { + result.invites = [] + } + + ;({ owner, ownerFeatures, members } = this.buildOwnerAndMembersViews( + members + )) + result.owner = owner + result.members = members + + result.features = _.defaults(ownerFeatures || {}, { + collaborators: -1, // Infinite + versioning: false, + dropbox: false, + compileTimeout: 60, + compileGroup: 'standard', + templates: false, + references: false, + referencesSearch: false, + mendeley: false, + trackChanges: false, + trackChangesVisible: ProjectEditorHandler.trackChangesAvailable + }) + + // Originally these two feature flags were both signalled by the now-deprecated `references` flag. + // For older users, the presence of the `references` feature flag should still turn on these features. + result.features.referencesSearch = + result.features.referencesSearch || result.features.references + result.features.mendeley = + result.features.mendeley || result.features.references + + return result + }, + + buildOwnerAndMembersViews(members) { + let owner = null + let ownerFeatures = null + const filteredMembers = [] + for (let member of Array.from(members || [])) { + if (member.privilegeLevel === 'owner') { + ownerFeatures = member.user.features + owner = this.buildUserModelView(member.user, 'owner') + } else { + filteredMembers.push( + this.buildUserModelView(member.user, member.privilegeLevel) + ) + } + } + return { + owner, + ownerFeatures, + members: filteredMembers + } + }, + + buildUserModelView(user, privileges) { + return { + _id: user._id, + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + privileges, + signUpDate: user.signUpDate + } + }, + + buildFolderModelView(folder) { + let file + const fileRefs = _.filter(folder.fileRefs || [], file => file != null) + return { + _id: folder._id, + name: folder.name, + folders: Array.from(folder.folders || []).map(childFolder => + this.buildFolderModelView(childFolder) + ), + fileRefs: (() => { + const result = [] + for (file of Array.from(fileRefs)) { + result.push(this.buildFileModelView(file)) + } + return result + })(), + docs: Array.from(folder.docs || []).map(doc => + this.buildDocModelView(doc) + ) + } + }, + + buildFileModelView(file) { + return { + _id: file._id, + name: file.name, + linkedFileData: file.linkedFileData, + created: file.created + } + }, + + buildDocModelView(doc) { + return { + _id: doc._id, + name: doc.name + } + } +} diff --git a/services/web/app/src/Features/Project/ProjectEntityHandler.js b/services/web/app/src/Features/Project/ProjectEntityHandler.js new file mode 100644 index 0000000000..8515efcbaf --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEntityHandler.js @@ -0,0 +1,297 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectEntityHandler, self +const _ = require('underscore') +const async = require('async') +const path = require('path') +const logger = require('logger-sharelatex') +const DocstoreManager = require('../Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') +const Errors = require('../Errors/Errors') +const { Project } = require('../../models/Project') +const ProjectGetter = require('./ProjectGetter') +const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') + +module.exports = ProjectEntityHandler = self = { + getAllDocs(project_id, callback) { + logger.log({ project_id }, 'getting all docs for project') + + // We get the path and name info from the project, and the lines and + // version info from the doc store. + return DocstoreManager.getAllDocs(project_id, function( + error, + docContentsArray + ) { + if (error != null) { + return callback(error) + } + + // Turn array from docstore into a dictionary based on doc id + const docContents = {} + for (let docContent of Array.from(docContentsArray)) { + docContents[docContent._id] = docContent + } + + return self._getAllFolders(project_id, function(error, folders) { + if (folders == null) { + folders = {} + } + if (error != null) { + return callback(error) + } + const docs = {} + for (let folderPath in folders) { + const folder = folders[folderPath] + for (let doc of Array.from(folder.docs || [])) { + const content = docContents[doc._id.toString()] + if (content != null) { + docs[path.join(folderPath, doc.name)] = { + _id: doc._id, + name: doc.name, + lines: content.lines, + rev: content.rev + } + } + } + } + logger.log( + { count: _.keys(docs).length, project_id }, + 'returning docs for project' + ) + return callback(null, docs) + }) + }) + }, + + getAllFiles(project_id, callback) { + logger.log({ project_id }, 'getting all files for project') + return self._getAllFolders(project_id, function(err, folders) { + if (folders == null) { + folders = {} + } + if (err != null) { + return callback(err) + } + const files = {} + for (let folderPath in folders) { + const folder = folders[folderPath] + for (let file of Array.from(folder.fileRefs || [])) { + if (file != null) { + files[path.join(folderPath, file.name)] = file + } + } + } + return callback(null, files) + }) + }, + + getAllEntities(project_id, callback) { + return ProjectGetter.getProject(project_id, function(err, project) { + if (err != null) { + return callback(err) + } + return self.getAllEntitiesFromProject(project, callback) + }) + }, + + getAllEntitiesFromProject(project, callback) { + logger.log({ project }, 'getting all entities for project') + return self._getAllFoldersFromProject(project, function(err, folders) { + if (folders == null) { + folders = {} + } + if (err != null) { + return callback(err) + } + const docs = [] + const files = [] + for (let folderPath in folders) { + const folder = folders[folderPath] + for (let doc of Array.from(folder.docs || [])) { + if (doc != null) { + docs.push({ path: path.join(folderPath, doc.name), doc }) + } + } + for (let file of Array.from(folder.fileRefs || [])) { + if (file != null) { + files.push({ path: path.join(folderPath, file.name), file }) + } + } + } + return callback(null, docs, files) + }) + }, + + getAllDocPathsFromProjectById(project_id, callback) { + return ProjectGetter.getProjectWithoutDocLines(project_id, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(Errors.NotFoundError('no project')) + } + return self.getAllDocPathsFromProject(project, callback) + }) + }, + + getAllDocPathsFromProject(project, callback) { + logger.log({ project }, 'getting all docs for project') + return self._getAllFoldersFromProject(project, function(err, folders) { + if (folders == null) { + folders = {} + } + if (err != null) { + return callback(err) + } + const docPath = {} + for (let folderPath in folders) { + const folder = folders[folderPath] + for (let doc of Array.from(folder.docs || [])) { + docPath[doc._id] = path.join(folderPath, doc.name) + } + } + logger.log( + { count: _.keys(docPath).length, project_id: project._id }, + 'returning docPaths for project' + ) + return callback(null, docPath) + }) + }, + + flushProjectToThirdPartyDataStore(project_id, callback) { + logger.log({ project_id }, 'flushing project to tpds') + return DocumentUpdaterHandler.flushProjectToMongo(project_id, function( + error + ) { + if (error != null) { + return callback(error) + } + return ProjectGetter.getProject(project_id, { name: true }, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + const requests = [] + return self.getAllDocs(project_id, function(error, docs) { + if (error != null) { + return callback(error) + } + for (let docPath in docs) { + const doc = docs[docPath] + ;((docPath, doc) => + requests.push(cb => + TpdsUpdateSender.addDoc( + { + project_id, + doc_id: doc._id, + path: docPath, + project_name: project.name, + rev: doc.rev || 0 + }, + cb + ) + ))(docPath, doc) + } + return self.getAllFiles(project_id, function(error, files) { + if (error != null) { + return callback(error) + } + for (let filePath in files) { + const file = files[filePath] + ;((filePath, file) => + requests.push(cb => + TpdsUpdateSender.addFile( + { + project_id, + file_id: file._id, + path: filePath, + project_name: project.name, + rev: file.rev + }, + cb + ) + ))(filePath, file) + } + return async.series(requests, function(err) { + logger.log({ project_id }, 'finished flushing project to tpds') + return callback(err) + }) + }) + }) + }) + }) + }, + + getDoc(project_id, doc_id, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function(error, lines, rev) {} + } + if (typeof options === 'function') { + callback = options + options = {} + } + + return DocstoreManager.getDoc(project_id, doc_id, options, callback) + }, + + _getAllFolders(project_id, callback) { + logger.log({ project_id }, 'getting all folders for project') + return ProjectGetter.getProjectWithoutDocLines(project_id, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(Errors.NotFoundError('no project')) + } + return self._getAllFoldersFromProject(project, callback) + }) + }, + + _getAllFoldersFromProject(project, callback) { + const folders = {} + var processFolder = function(basePath, folder) { + folders[basePath] = folder + return (() => { + const result = [] + for (let childFolder of Array.from(folder.folders || [])) { + if (childFolder.name != null) { + result.push( + processFolder(path.join(basePath, childFolder.name), childFolder) + ) + } else { + result.push(undefined) + } + } + return result + })() + } + + processFolder('/', project.rootFolder[0]) + return callback(null, folders) + } +} diff --git a/services/web/app/src/Features/Project/ProjectEntityMongoUpdateHandler.js b/services/web/app/src/Features/Project/ProjectEntityMongoUpdateHandler.js new file mode 100644 index 0000000000..25cb3ec80a --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEntityMongoUpdateHandler.js @@ -0,0 +1,888 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + one-var, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectEntityMongoUpdateHandler, self +const _ = require('underscore') +const async = require('async') +const logger = require('logger-sharelatex') +const path = require('path') +const settings = require('settings-sharelatex') +const CooldownManager = require('../Cooldown/CooldownManager') +const Errors = require('../Errors/Errors') +const { Folder } = require('../../models/Folder') +const LockManager = require('../../infrastructure/LockManager') +const { Project } = require('../../models/Project') +const ProjectEntityHandler = require('./ProjectEntityHandler') +const ProjectGetter = require('./ProjectGetter') +const ProjectLocator = require('./ProjectLocator') +const SafePath = require('./SafePath') + +const LOCK_NAMESPACE = 'mongoTransaction' + +const wrapWithLock = function(methodWithoutLock) { + // This lock is used whenever we read or write to an existing project's + // structure. Some operations to project structure cannot be done atomically + // in mongo, this lock is used to prevent reading the structure between two + // parts of a staged update. + const methodWithLock = function(project_id, ...rest) { + const adjustedLength = Math.max(rest.length, 1), + args = rest.slice(0, adjustedLength - 1), + callback = rest[adjustedLength - 1] + return LockManager.runWithLock( + LOCK_NAMESPACE, + project_id, + cb => methodWithoutLock(project_id, ...Array.from(args), cb), + callback + ) + } + methodWithLock.withoutLock = methodWithoutLock + return methodWithLock +} + +module.exports = ProjectEntityMongoUpdateHandler = self = { + LOCK_NAMESPACE, + + addDoc: wrapWithLock(function(project_id, folder_id, doc, callback) { + if (callback == null) { + callback = function(err, result) {} + } + return ProjectGetter.getProjectWithoutLock( + project_id, + { rootFolder: true, name: true, overleaf: true }, + function(err, project) { + if (err != null) { + logger.err({ project_id, err }, 'error getting project for add doc') + return callback(err) + } + logger.log( + { project_id, folder_id, doc_name: doc.name }, + 'adding doc to project with project' + ) + return self._confirmFolder(project, folder_id, folder_id => { + return self._putElement(project, folder_id, doc, 'doc', callback) + }) + } + ) + }), + + addFile: wrapWithLock(function(project_id, folder_id, fileRef, callback) { + if (callback == null) { + callback = function(error, result, project) {} + } + return ProjectGetter.getProjectWithoutLock( + project_id, + { rootFolder: true, name: true, overleaf: true }, + function(err, project) { + if (err != null) { + logger.err({ project_id, err }, 'error getting project for add file') + return callback(err) + } + logger.log( + { project_id: project._id, folder_id, file_name: fileRef.name }, + 'adding file' + ) + return self._confirmFolder(project, folder_id, folder_id => + self._putElement(project, folder_id, fileRef, 'file', callback) + ) + } + ) + }), + + replaceFileWithNew: wrapWithLock( + (project_id, file_id, newFileRef, callback) => + ProjectGetter.getProjectWithoutLock( + project_id, + { rootFolder: true, name: true, overleaf: true }, + function(err, project) { + if (err != null) { + return callback(err) + } + return ProjectLocator.findElement( + { project, element_id: file_id, type: 'file' }, + (err, fileRef, path) => { + if (err != null) { + return callback(err) + } + return ProjectEntityMongoUpdateHandler._insertDeletedFileReference( + project_id, + fileRef, + function(err) { + if (err != null) { + return callback(err) + } + const conditions = { _id: project._id } + const inc = {} + // increment the project structure version as we are adding a new file here + inc['version'] = 1 + const set = {} + set[`${path.mongo}._id`] = newFileRef._id + set[`${path.mongo}.created`] = new Date() + set[`${path.mongo}.linkedFileData`] = + newFileRef.linkedFileData + inc[`${path.mongo}.rev`] = 1 + set[`${path.mongo}.hash`] = newFileRef.hash + const update = { + $inc: inc, + $set: set + } + // Note: Mongoose uses new:true to return the modified document + // https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate + // but Mongo uses returnNewDocument:true instead + // https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/ + // We are using Mongoose here, but if we ever switch to a direct mongo call + // the next line will need to be updated. + return Project.findOneAndUpdate( + conditions, + update, + { new: true }, + function(err, newProject) { + if (err != null) { + return callback(err) + } + return callback(null, fileRef, project, path, newProject) + } + ) + } + ) + } + ) + } + ) + ), + + mkdirp: wrapWithLock(function(project_id, path, options, callback) { + // defaults to case insensitive paths, use options {exactCaseMatch:true} + // to make matching case-sensitive + let folders = path.split('/') + folders = _.select(folders, folder => folder.length !== 0) + + return ProjectGetter.getProjectWithOnlyFolders( + project_id, + (err, project) => { + if (path === '/') { + logger.log( + { project_id: project._id }, + 'mkdir is only trying to make path of / so sending back root folder' + ) + return callback(null, [], project.rootFolder[0]) + } + logger.log({ project_id: project._id, path, folders }, 'running mkdirp') + + let builtUpPath = '' + const procesFolder = (previousFolders, folderName, callback) => { + let parentFolder_id + previousFolders = previousFolders || [] + const parentFolder = previousFolders[previousFolders.length - 1] + if (parentFolder != null) { + parentFolder_id = parentFolder._id + } + builtUpPath = `${builtUpPath}/${folderName}` + return ProjectLocator.findElementByPath( + { + project, + path: builtUpPath, + exactCaseMatch: + options != null ? options.exactCaseMatch : undefined + }, + (err, foundFolder) => { + if (foundFolder == null) { + logger.log( + { path, project_id: project._id, folderName }, + 'making folder from mkdirp' + ) + return self.addFolder.withoutLock( + project_id, + parentFolder_id, + folderName, + function(err, newFolder, parentFolder_id) { + if (err != null) { + return callback(err) + } + newFolder.parentFolder_id = parentFolder_id + previousFolders.push(newFolder) + return callback(null, previousFolders) + } + ) + } else { + foundFolder.filterOut = true + previousFolders.push(foundFolder) + return callback(null, previousFolders) + } + } + ) + } + + return async.reduce(folders, [], procesFolder, function(err, folders) { + if (err != null) { + return callback(err) + } + const lastFolder = folders[folders.length - 1] + folders = _.select(folders, folder => !folder.filterOut) + return callback(null, folders, lastFolder) + }) + } + ) + }), + + moveEntity: wrapWithLock(function( + project_id, + entity_id, + destFolderId, + entityType, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + return ProjectGetter.getProjectWithoutLock( + project_id, + { rootFolder: true, name: true, overleaf: true }, + function(err, project) { + if (err != null) { + return callback(err) + } + return ProjectLocator.findElement( + { project, element_id: entity_id, type: entityType }, + function(err, entity, entityPath) { + if (err != null) { + return callback(err) + } + // Prevent top-level docs/files with reserved names (to match v1 behaviour) + if (self._blockedFilename(entityPath, entityType)) { + return callback( + new Errors.InvalidNameError('blocked element name') + ) + } + return self._checkValidMove( + project, + entityType, + entity, + entityPath, + destFolderId, + function(error) { + if (error != null) { + return callback(error) + } + return ProjectEntityHandler.getAllEntitiesFromProject( + project, + function(error, oldDocs, oldFiles) { + if (error != null) { + return callback(error) + } + // For safety, insert the entity in the destination + // location first, and then remove the original. If + // there is an error the entity may appear twice. This + // will cause some breakage but is better than being + // lost, which is what happens if this is done in the + // opposite order. + return self._putElement( + project, + destFolderId, + entity, + entityType, + function(err, result) { + if (err != null) { + return callback(err) + } + // Note: putElement always pushes onto the end of an + // array so it will never change an existing mongo + // path. Therefore it is safe to remove an element + // from the project with an existing path after + // calling putElement. But we must be sure that we + // have not moved a folder subfolder of itself (which + // is done by _checkValidMove above) because that + // would lead to it being deleted. + return self._removeElementFromMongoArray( + Project, + project_id, + entityPath.mongo, + entity_id, + function(err, newProject) { + if (err != null) { + return callback(err) + } + return ProjectEntityHandler.getAllEntitiesFromProject( + newProject, + function(err, newDocs, newFiles) { + if (err != null) { + return callback(err) + } + const startPath = entityPath.fileSystem + const endPath = result.path.fileSystem + const changes = { + oldDocs, + newDocs, + oldFiles, + newFiles, + newProject + } + // check that no files have been lost (or duplicated) + if ( + oldFiles.length !== newFiles.length || + oldDocs.length !== newDocs.length + ) { + logger.err( + { + project_id, + oldDocs: oldDocs.length, + newDocs: newDocs.length, + oldFiles: oldFiles.length, + newFiles: newFiles.length, + origProject: project, + newProject + }, + "project corrupted moving files - shouldn't happen" + ) + return callback( + new Error( + 'unexpected change in project structure' + ) + ) + } + return callback( + null, + project, + startPath, + endPath, + entity.rev, + changes, + callback + ) + } + ) + } + ) + } + ) + } + ) + } + ) + } + ) + } + ) + }), + + deleteEntity: wrapWithLock((project_id, entity_id, entityType, callback) => + ProjectGetter.getProjectWithoutLock( + project_id, + { name: true, rootFolder: true, overleaf: true }, + function(error, project) { + if (error != null) { + return callback(error) + } + return ProjectLocator.findElement( + { project, element_id: entity_id, type: entityType }, + function(error, entity, path) { + if (error != null) { + return callback(error) + } + return self._removeElementFromMongoArray( + Project, + project_id, + path.mongo, + entity_id, + function(error, newProject) { + if (error != null) { + return callback(error) + } + return callback(null, entity, path, project, newProject) + } + ) + } + ) + } + ) + ), + + renameEntity: wrapWithLock( + (project_id, entity_id, entityType, newName, callback) => + ProjectGetter.getProjectWithoutLock( + project_id, + { rootFolder: true, name: true, overleaf: true }, + (error, project) => { + if (error != null) { + return callback(error) + } + return ProjectEntityHandler.getAllEntitiesFromProject( + project, + (error, oldDocs, oldFiles) => { + if (error != null) { + return callback(error) + } + return ProjectLocator.findElement( + { project, element_id: entity_id, type: entityType }, + (error, entity, entPath, parentFolder) => { + if (error != null) { + return callback(error) + } + const endPath = path.join( + path.dirname(entPath.fileSystem), + newName + ) + // Prevent top-level docs/files with reserved names (to match v1 behaviour) + if ( + self._blockedFilename({ fileSystem: endPath }, entityType) + ) { + return callback( + new Errors.InvalidNameError('blocked element name') + ) + } + // check if the new name already exists in the current folder + return self._checkValidElementName( + parentFolder, + newName, + error => { + if (error != null) { + return callback(error) + } + const conditions = { _id: project_id } + const update = { $set: {}, $inc: {} } + const namePath = entPath.mongo + '.name' + update['$set'][namePath] = newName + // we need to increment the project version number for any structure change + update['$inc']['version'] = 1 + return Project.findOneAndUpdate( + conditions, + update, + { new: true }, + function(error, newProject) { + if (error != null) { + return callback(error) + } + return ProjectEntityHandler.getAllEntitiesFromProject( + newProject, + (error, newDocs, newFiles) => { + if (error != null) { + return callback(error) + } + const startPath = entPath.fileSystem + const changes = { + oldDocs, + newDocs, + oldFiles, + newFiles, + newProject + } + return callback( + null, + project, + startPath, + endPath, + entity.rev, + changes, + callback + ) + } + ) + } + ) + } + ) + } + ) + } + ) + } + ) + ), + + addFolder: wrapWithLock((project_id, parentFolder_id, folderName, callback) => + ProjectGetter.getProjectWithoutLock( + project_id, + { rootFolder: true, name: true, overleaf: true }, + function(err, project) { + if (err != null) { + logger.err( + { project_id, err }, + 'error getting project for add folder' + ) + return callback(err) + } + return self._confirmFolder( + project, + parentFolder_id, + parentFolder_id => { + const folder = new Folder({ name: folderName }) + logger.log( + { project: project._id, parentFolder_id, folderName }, + 'adding new folder' + ) + return self._putElement( + project, + parentFolder_id, + folder, + 'folder', + err => { + if (err != null) { + logger.err( + { err, project_id: project._id }, + 'error adding folder to project' + ) + return callback(err) + } + return callback(null, folder, parentFolder_id) + } + ) + } + ) + } + ) + ), + + _removeElementFromMongoArray(model, model_id, path, element_id, callback) { + if (callback == null) { + callback = function(err, project) {} + } + const conditions = { _id: model_id } + const pullUpdate = { $pull: {}, $inc: {} } + const nonArrayPath = path.slice(0, path.lastIndexOf('.')) + // remove specific element from array by id + pullUpdate['$pull'][nonArrayPath] = { _id: element_id } + // we need to increment the project version number for any structure change + pullUpdate['$inc']['version'] = 1 + return model.findOneAndUpdate( + conditions, + pullUpdate, + { new: true }, + callback + ) + }, + + _countElements(project) { + var countFolder = function(folder) { + let total = 0 + + for (let subfolder of Array.from( + (folder != null ? folder.folders : undefined) || [] + )) { + total += countFolder(subfolder) + } + + if ( + __guard__(folder != null ? folder.folders : undefined, x => x.length) != + null + ) { + total += folder.folders.length + } + + if ( + __guard__(folder != null ? folder.docs : undefined, x1 => x1.length) != + null + ) { + total += folder.docs.length + } + + if ( + __guard__( + folder != null ? folder.fileRefs : undefined, + x2 => x2.length + ) != null + ) { + total += folder.fileRefs.length + } + + return total + } + + return countFolder(project.rootFolder[0]) + }, + + _putElement(project, folder_id, element, type, callback) { + let e + if (callback == null) { + callback = function(err, path, project) {} + } + const sanitizeTypeOfElement = function(elementType) { + const lastChar = elementType.slice(-1) + if (lastChar !== 's') { + elementType += 's' + } + if (elementType === 'files') { + elementType = 'fileRefs' + } + return elementType + } + + if (element == null || element._id == null) { + e = new Error('no element passed to be inserted') + logger.err( + { project_id: project._id, folder_id, element, type }, + 'failed trying to insert element as it was null' + ) + return callback(e) + } + type = sanitizeTypeOfElement(type) + + // original check path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/") + // check if name is allowed + if (!SafePath.isCleanFilename(element.name)) { + e = new Errors.InvalidNameError('invalid element name') + logger.err( + { project_id: project._id, folder_id, element, type }, + 'failed trying to insert element as name was invalid' + ) + return callback(e) + } + + if (folder_id == null) { + folder_id = project.rootFolder[0]._id + } + + if (self._countElements(project) > settings.maxEntitiesPerProject) { + logger.warn( + { project_id: project._id }, + 'project too big, stopping insertions' + ) + CooldownManager.putProjectOnCooldown(project._id) + return callback('project_has_to_many_files') + } + + return ProjectLocator.findElement( + { project, element_id: folder_id, type: 'folders' }, + (err, folder, path) => { + if (err != null) { + logger.err( + { err, project_id: project._id, folder_id, type, element }, + 'error finding folder for _putElement' + ) + return callback(err) + } + const newPath = { + fileSystem: `${path.fileSystem}/${element.name}`, + mongo: path.mongo + } + // check if the path would be too long + if (!SafePath.isAllowedLength(newPath.fileSystem)) { + return callback(new Errors.InvalidNameError('path too long')) + } + // Prevent top-level docs/files with reserved names (to match v1 behaviour) + if (self._blockedFilename(newPath, type)) { + return callback(new Errors.InvalidNameError('blocked element name')) + } + return self._checkValidElementName(folder, element.name, err => { + if (err != null) { + return callback(err) + } + const id = element._id + '' + element._id = require('mongoose').Types.ObjectId(id) + const conditions = { _id: project._id } + const mongopath = `${path.mongo}.${type}` + const update = { $push: {}, $inc: {} } + update['$push'][mongopath] = element + // we need to increment the project version number for any structure change + update['$inc']['version'] = 1 // increment project version number + logger.log( + { + project_id: project._id, + element_id: element._id, + fileType: type, + folder_id, + mongopath + }, + 'adding element to project' + ) + // We are using Mongoose here, but if we ever switch to a direct mongo call + // the next line will need to be updated to {returnNewDocument:true} + return Project.findOneAndUpdate( + conditions, + update, + { new: true }, + function(err, newProject) { + if (err != null) { + logger.err( + { err, project_id: project._id }, + 'error saving in putElement project' + ) + return callback(err) + } + return callback(err, { path: newPath }, newProject) + } + ) + }) + } + ) + }, + + _blockedFilename(entityPath, entityType) { + // check if name would be blocked in v1 + // javascript reserved names are forbidden for docs and files + // at the top-level (but folders with reserved names are allowed). + const isFolder = ['folder', 'folders'].includes(entityType) + const [dir, file] = Array.from([ + path.dirname(entityPath.fileSystem), + path.basename(entityPath.fileSystem) + ]) + const isTopLevel = dir === '/' + if (isTopLevel && !isFolder && SafePath.isBlockedFilename(file)) { + return true + } else { + return false + } + }, + + _checkValidElementName(folder, name, callback) { + // check if the name is already taken by a doc, file or + // folder. If so, return an error "file already exists". + if (callback == null) { + callback = function(err) {} + } + const err = new Errors.InvalidNameError('file already exists') + for (let doc of Array.from( + (folder != null ? folder.docs : undefined) || [] + )) { + if (doc.name === name) { + return callback(err) + } + } + for (let file of Array.from( + (folder != null ? folder.fileRefs : undefined) || [] + )) { + if (file.name === name) { + return callback(err) + } + } + for (folder of Array.from( + (folder != null ? folder.folders : undefined) || [] + )) { + if (folder.name === name) { + return callback(err) + } + } + return callback() + }, + + _confirmFolder(project, folder_id, callback) { + logger.log( + { folder_id, project_id: project._id }, + 'confirming folder in project' + ) + if (folder_id + '' === 'undefined') { + return callback(project.rootFolder[0]._id) + } else if (folder_id !== null) { + return callback(folder_id) + } else { + return callback(project.rootFolder[0]._id) + } + }, + + _checkValidMove( + project, + entityType, + entity, + entityPath, + destFolderId, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + return ProjectLocator.findElement( + { project, element_id: destFolderId, type: 'folder' }, + function(err, destEntity, destFolderPath) { + if (err != null) { + return callback(err) + } + // check if there is already a doc/file/folder with the same name + // in the destination folder + return self._checkValidElementName(destEntity, entity.name, function( + err + ) { + if (err != null) { + return callback(err) + } + if (/folder/.test(entityType)) { + logger.log( + { + destFolderPath: destFolderPath.fileSystem, + folderPath: entityPath.fileSystem + }, + 'checking folder is not moving into child folder' + ) + const isNestedFolder = + destFolderPath.fileSystem.slice( + 0, + entityPath.fileSystem.length + ) === entityPath.fileSystem + if (isNestedFolder) { + return callback( + new Errors.InvalidNameError( + 'destination folder is a child folder of me' + ) + ) + } + } + return callback() + }) + } + ) + }, + + _insertDeletedDocReference(project_id, doc, callback) { + if (callback == null) { + callback = function(error) {} + } + return Project.update( + { + _id: project_id + }, + { + $push: { + deletedDocs: { + _id: doc._id, + name: doc.name, + deletedAt: new Date() + } + } + }, + {}, + callback + ) + }, + + _insertDeletedFileReference(project_id, fileRef, callback) { + if (callback == null) { + callback = function(error) {} + } + return Project.update( + { + _id: project_id + }, + { + $push: { + deletedFiles: { + _id: fileRef._id, + name: fileRef.name, + linkedFileData: fileRef.linkedFileData, + hash: fileRef.hash, + deletedAt: new Date() + } + } + }, + {}, + callback + ) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js b/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js new file mode 100644 index 0000000000..1f290efd57 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js @@ -0,0 +1,1594 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + one-var, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectEntityUpdateHandler, self +const _ = require('lodash') +const async = require('async') +const logger = require('logger-sharelatex') +const path = require('path') +const { Doc } = require('../../models/Doc') +const DocstoreManager = require('../Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') +const Errors = require('../Errors/Errors') +const { File } = require('../../models/File') +const FileStoreHandler = require('../FileStore/FileStoreHandler') +const LockManager = require('../../infrastructure/LockManager') +const { Project } = require('../../models/Project') +const ProjectEntityHandler = require('./ProjectEntityHandler') +const ProjectGetter = require('./ProjectGetter') +const ProjectLocator = require('./ProjectLocator') +const ProjectUpdateHandler = require('./ProjectUpdateHandler') +const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') +const SafePath = require('./SafePath') +const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') + +const LOCK_NAMESPACE = 'sequentialProjectStructureUpdateLock' + +const wrapWithLock = function(methodWithoutLock) { + // This lock is used to make sure that the project structure updates are made + // sequentially. In particular the updates must be made in mongo and sent to + // the doc-updater in the same order. + let methodWithLock + if (typeof methodWithoutLock === 'function') { + methodWithLock = function(project_id, ...rest) { + const adjustedLength = Math.max(rest.length, 1), + args = rest.slice(0, adjustedLength - 1), + callback = rest[adjustedLength - 1] + return LockManager.runWithLock( + LOCK_NAMESPACE, + project_id, + cb => methodWithoutLock(project_id, ...Array.from(args), cb), + callback + ) + } + methodWithLock.withoutLock = methodWithoutLock + return methodWithLock + } else { + // handle case with separate setup and locked stages + const wrapWithSetup = methodWithoutLock.beforeLock // a function to set things up before the lock + const mainTask = methodWithoutLock.withLock // function to execute inside the lock + methodWithLock = wrapWithSetup(function(project_id, ...rest) { + const adjustedLength = Math.max(rest.length, 1), + args = rest.slice(0, adjustedLength - 1), + callback = rest[adjustedLength - 1] + return LockManager.runWithLock( + LOCK_NAMESPACE, + project_id, + cb => mainTask(project_id, ...Array.from(args), cb), + callback + ) + }) + methodWithLock.withoutLock = wrapWithSetup(mainTask) + methodWithLock.beforeLock = methodWithoutLock.beforeLock + methodWithLock.mainTask = methodWithoutLock.withLock + return methodWithLock + } +} + +module.exports = ProjectEntityUpdateHandler = self = { + copyFileFromExistingProjectWithProject: wrapWithLock({ + beforeLock(next) { + return function( + project_id, + project, + folder_id, + originalProject_id, + origonalFileRef, + userId, + callback + ) { + if (callback == null) { + callback = function(error, fileRef, folder_id) {} + } + logger.log( + { project_id, folder_id, originalProject_id, origonalFileRef }, + 'copying file in s3 with project' + ) + return ProjectEntityMongoUpdateHandler._confirmFolder( + project, + folder_id, + function(folder_id) { + if (origonalFileRef == null) { + logger.err( + { project_id, folder_id, originalProject_id, origonalFileRef }, + 'file trying to copy is null' + ) + return callback() + } + // convert any invalid characters in original file to '_' + const fileProperties = { + name: SafePath.clean(origonalFileRef.name) + } + if (origonalFileRef.linkedFileData != null) { + fileProperties.linkedFileData = origonalFileRef.linkedFileData + } + if (origonalFileRef.hash != null) { + fileProperties.hash = origonalFileRef.hash + } + const fileRef = new File(fileProperties) + return FileStoreHandler.copyFile( + originalProject_id, + origonalFileRef._id, + project._id, + fileRef._id, + function(err, fileStoreUrl) { + if (err != null) { + logger.err( + { + err, + project_id, + folder_id, + originalProject_id, + origonalFileRef + }, + 'error coping file in s3' + ) + return callback(err) + } + return next( + project_id, + project, + folder_id, + originalProject_id, + origonalFileRef, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + ) + } + }, + withLock( + project_id, + project, + folder_id, + originalProject_id, + origonalFileRef, + userId, + fileRef, + fileStoreUrl, + callback + ) { + if (callback == null) { + callback = function(error, fileRef, folder_id) {} + } + const projectHistoryId = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ) + return ProjectEntityMongoUpdateHandler._putElement( + project, + folder_id, + fileRef, + 'file', + function(err, result, newProject) { + if (err != null) { + logger.err( + { err, project_id, folder_id }, + 'error putting element as part of copy' + ) + return callback(err) + } + return TpdsUpdateSender.addFile( + { + project_id, + file_id: fileRef._id, + path: __guard__( + result != null ? result.path : undefined, + x1 => x1.fileSystem + ), + rev: fileRef.rev, + project_name: project.name + }, + function(err) { + if (err != null) { + logger.err( + { + err, + project_id, + folder_id, + originalProject_id, + origonalFileRef + }, + 'error sending file to tpds worker' + ) + } + const newFiles = [ + { + file: fileRef, + path: __guard__( + result != null ? result.path : undefined, + x2 => x2.fileSystem + ), + url: fileStoreUrl + } + ] + return DocumentUpdaterHandler.updateProjectStructure( + project_id, + projectHistoryId, + userId, + { newFiles, newProject }, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, fileRef, folder_id) + } + ) + } + ) + } + ) + } + }), + + updateDocLines( + project_id, + doc_id, + lines, + version, + ranges, + lastUpdatedAt, + lastUpdatedBy, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + return ProjectGetter.getProjectWithoutDocLines(project_id, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('project not found')) + } + logger.log({ project_id, doc_id }, 'updating doc lines') + return ProjectLocator.findElement( + { project, element_id: doc_id, type: 'docs' }, + function(err, doc, path) { + let isDeletedDoc = false + if (err != null) { + if (err instanceof Errors.NotFoundError) { + // We need to be able to update the doclines of deleted docs. This is + // so the doc-updater can flush a doc's content to the doc-store after + // the doc is deleted. + isDeletedDoc = true + doc = _.find( + project.deletedDocs, + doc => doc._id.toString() === doc_id.toString() + ) + } else { + return callback(err) + } + } + + if (doc == null) { + // Do not allow an update to a doc which has never exist on this project + logger.error( + { doc_id, project_id, lines }, + 'doc not found while updating doc lines' + ) + return callback(new Errors.NotFoundError('doc not found')) + } + + logger.log( + { project_id, doc_id }, + 'telling docstore manager to update doc' + ) + return DocstoreManager.updateDoc( + project_id, + doc_id, + lines, + version, + ranges, + function(err, modified, rev) { + if (err != null) { + logger.error( + { err, doc_id, project_id, lines }, + 'error sending doc to docstore' + ) + return callback(err) + } + logger.log( + { project_id, doc_id, modified }, + 'finished updating doc lines' + ) + // path will only be present if the doc is not deleted + if (modified && !isDeletedDoc) { + // Don't need to block for marking as updated + ProjectUpdateHandler.markAsUpdated( + project_id, + lastUpdatedAt, + lastUpdatedBy + ) + return TpdsUpdateSender.addDoc( + { + project_id, + path: path.fileSystem, + doc_id, + project_name: project.name, + rev + }, + callback + ) + } else { + return callback() + } + } + ) + } + ) + }) + }, + + setRootDoc(project_id, newRootDocID, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id, rootDocId: newRootDocID }, 'setting root doc') + return Project.update( + { _id: project_id }, + { rootDoc_id: newRootDocID }, + {}, + callback + ) + }, + + unsetRootDoc(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id }, 'removing root doc') + return Project.update( + { _id: project_id }, + { $unset: { rootDoc_id: true } }, + {}, + callback + ) + }, + + _addDocAndSendToTpds(project_id, folder_id, doc, callback) { + if (callback == null) { + callback = function(error, result, project) {} + } + return ProjectEntityMongoUpdateHandler.addDoc( + project_id, + folder_id, + doc, + function(err, result, project) { + if (err != null) { + logger.err( + { + err, + project_id, + folder_id, + doc_name: doc != null ? doc.name : undefined, + doc_id: doc != null ? doc._id : undefined + }, + 'error adding file with project' + ) + return callback(err) + } + return TpdsUpdateSender.addDoc( + { + project_id, + doc_id: doc != null ? doc._id : undefined, + path: __guard__( + result != null ? result.path : undefined, + x => x.fileSystem + ), + project_name: project.name, + rev: 0 + }, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, result, project) + } + ) + } + ) + }, + + addDoc(project_id, folder_id, docName, docLines, userId, callback) { + return self.addDocWithRanges( + project_id, + folder_id, + docName, + docLines, + {}, + userId, + callback + ) + }, + + addDocWithRanges: wrapWithLock({ + beforeLock(next) { + return function( + project_id, + folder_id, + docName, + docLines, + ranges, + userId, + callback + ) { + if (callback == null) { + callback = function(error, doc, folder_id) {} + } + if (!SafePath.isCleanFilename(docName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + // Put doc in docstore first, so that if it errors, we don't have a doc_id in the project + // which hasn't been created in docstore. + const doc = new Doc({ name: docName }) + return DocstoreManager.updateDoc( + project_id.toString(), + doc._id.toString(), + docLines, + 0, + ranges, + function(err, modified, rev) { + if (err != null) { + return callback(err) + } + return next( + project_id, + folder_id, + doc, + docName, + docLines, + ranges, + userId, + callback + ) + } + ) + } + }, + withLock( + project_id, + folder_id, + doc, + docName, + docLines, + ranges, + userId, + callback + ) { + if (callback == null) { + callback = function(error, doc, folder_id) {} + } + return ProjectEntityUpdateHandler._addDocAndSendToTpds( + project_id, + folder_id, + doc, + function(err, result, project) { + if (err != null) { + return callback(err) + } + const docPath = __guard__( + result != null ? result.path : undefined, + x => x.fileSystem + ) + const projectHistoryId = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x1 => x1.id + ) + const newDocs = [ + { + doc, + path: docPath, + docLines: docLines.join('\n') + } + ] + return DocumentUpdaterHandler.updateProjectStructure( + project_id, + projectHistoryId, + userId, + { newDocs, newProject: project }, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, doc, folder_id) + } + ) + } + ) + } + }), + + _uploadFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + callback + ) { + if (callback == null) { + callback = function(error, fileStoreUrl, fileRef) {} + } + if (!SafePath.isCleanFilename(fileName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + const fileArgs = { + name: fileName, + linkedFileData + } + return FileStoreHandler.uploadFileFromDisk( + project_id, + fileArgs, + fsPath, + function(err, fileStoreUrl, fileRef) { + if (err != null) { + logger.err( + { err, project_id, folder_id, file_name: fileName, fileRef }, + 'error uploading image to s3' + ) + return callback(err) + } + return callback(null, fileStoreUrl, fileRef) + } + ) + }, + + _addFileAndSendToTpds(project_id, folder_id, fileRef, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectEntityMongoUpdateHandler.addFile( + project_id, + folder_id, + fileRef, + function(err, result, project) { + if (err != null) { + logger.err( + { err, project_id, folder_id, file_name: fileRef.name, fileRef }, + 'error adding file with project' + ) + return callback(err) + } + return TpdsUpdateSender.addFile( + { + project_id, + file_id: fileRef._id, + path: __guard__( + result != null ? result.path : undefined, + x => x.fileSystem + ), + project_name: project.name, + rev: fileRef.rev + }, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, result, project) + } + ) + } + ) + }, + + addFile: wrapWithLock({ + beforeLock(next) { + return function( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + userId, + callback + ) { + if (!SafePath.isCleanFilename(fileName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + return ProjectEntityUpdateHandler._uploadFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + function(error, fileStoreUrl, fileRef) { + if (error != null) { + return callback(error) + } + return next( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) { + if (callback == null) { + callback = function(error, fileRef, folder_id) {} + } + return ProjectEntityUpdateHandler._addFileAndSendToTpds( + project_id, + folder_id, + fileRef, + function(err, result, project) { + if (err != null) { + return callback(err) + } + const projectHistoryId = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ) + const newFiles = [ + { + file: fileRef, + path: __guard__( + result != null ? result.path : undefined, + x1 => x1.fileSystem + ), + url: fileStoreUrl + } + ] + return DocumentUpdaterHandler.updateProjectStructure( + project_id, + projectHistoryId, + userId, + { newFiles, newProject: project }, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, fileRef, folder_id) + } + ) + } + ) + } + }), + + replaceFile: wrapWithLock({ + beforeLock(next) { + return function( + project_id, + file_id, + fsPath, + linkedFileData, + userId, + callback + ) { + // create a new file + const fileArgs = { + name: 'dummy-upload-filename', + linkedFileData + } + return FileStoreHandler.uploadFileFromDisk( + project_id, + fileArgs, + fsPath, + function(err, fileStoreUrl, fileRef) { + if (err != null) { + return callback(err) + } + return next( + project_id, + file_id, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + project_id, + file_id, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + callback + ) { + return ProjectEntityMongoUpdateHandler.replaceFileWithNew( + project_id, + file_id, + newFileRef, + function(err, oldFileRef, project, path, newProject) { + if (err != null) { + return callback(err) + } + const oldFiles = [ + { + file: oldFileRef, + path: path.fileSystem + } + ] + const newFiles = [ + { + file: newFileRef, + path: path.fileSystem, + url: fileStoreUrl + } + ] + const projectHistoryId = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ) + // Increment the rev for an in-place update (with the same path) so the third-party-datastore + // knows this is a new file. + // Ideally we would get this from ProjectEntityMongoUpdateHandler.replaceFileWithNew + // but it returns the original oldFileRef (after incrementing the rev value in mongo), + // so we add 1 to the rev from that. This isn't atomic and relies on the lock + // but it is acceptable for now. + return TpdsUpdateSender.addFile( + { + project_id: project._id, + file_id: newFileRef._id, + path: path.fileSystem, + rev: oldFileRef.rev + 1, + project_name: project.name + }, + function(err) { + if (err != null) { + return callback(err) + } + return DocumentUpdaterHandler.updateProjectStructure( + project_id, + projectHistoryId, + userId, + { oldFiles, newFiles, newProject }, + callback + ) + } + ) + } + ) + } + }), + + upsertDoc: wrapWithLock(function( + project_id, + folder_id, + docName, + docLines, + source, + userId, + callback + ) { + if (callback == null) { + callback = function(err, doc, folder_id, isNewDoc) {} + } + if (!SafePath.isCleanFilename(docName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + return ProjectLocator.findElement( + { project_id, element_id: folder_id, type: 'folder' }, + function(error, folder) { + if (error != null) { + return callback(error) + } + if (folder == null) { + return callback(new Error("Couldn't find folder")) + } + let existingDoc = null + for (let doc of Array.from(folder.docs)) { + if (doc.name === docName) { + existingDoc = doc + break + } + } + if (existingDoc != null) { + return DocumentUpdaterHandler.setDocument( + project_id, + existingDoc._id, + userId, + docLines, + source, + err => { + logger.log( + { project_id, doc_id: existingDoc._id }, + 'notifying users that the document has been updated' + ) + return DocumentUpdaterHandler.flushDocToMongo( + project_id, + existingDoc._id, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, existingDoc, existingDoc == null) + } + ) + } + ) + } else { + return self.addDocWithRanges.withoutLock( + project_id, + folder_id, + docName, + docLines, + {}, + userId, + function(err, doc) { + if (err != null) { + return callback(err) + } + return callback(null, doc, existingDoc == null) + } + ) + } + } + ) + }), + + upsertFile: wrapWithLock({ + beforeLock(next) { + return function( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + userId, + callback + ) { + if (!SafePath.isCleanFilename(fileName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + // create a new file + const fileArgs = { + name: fileName, + linkedFileData + } + return FileStoreHandler.uploadFileFromDisk( + project_id, + fileArgs, + fsPath, + function(err, fileStoreUrl, fileRef) { + if (err != null) { + return callback(err) + } + return next( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + callback + ) { + if (callback == null) { + callback = function(err, file, isNewFile, existingFile) {} + } + return ProjectLocator.findElement( + { project_id, element_id: folder_id, type: 'folder' }, + function(error, folder) { + if (error != null) { + return callback(error) + } + if (folder == null) { + return callback(new Error("Couldn't find folder")) + } + let existingFile = null + for (let fileRef of Array.from(folder.fileRefs)) { + if (fileRef.name === fileName) { + existingFile = fileRef + break + } + } + if (existingFile != null) { + // this calls directly into the replaceFile main task (without the beforeLock part) + return self.replaceFile.mainTask( + project_id, + existingFile._id, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + function(err) { + if (err != null) { + return callback(err) + } + return callback( + null, + newFileRef, + existingFile == null, + existingFile + ) + } + ) + } else { + // this calls directly into the addFile main task (without the beforeLock part) + return self.addFile.mainTask( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + function(err) { + if (err != null) { + return callback(err) + } + return callback( + null, + newFileRef, + existingFile == null, + existingFile + ) + } + ) + } + } + ) + } + }), + + upsertDocWithPath: wrapWithLock(function( + project_id, + elementPath, + docLines, + source, + userId, + callback + ) { + if (!SafePath.isCleanPath(elementPath)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + const docName = path.basename(elementPath) + const folderPath = path.dirname(elementPath) + return self.mkdirp.withoutLock(project_id, folderPath, function( + err, + newFolders, + folder + ) { + if (err != null) { + return callback(err) + } + return self.upsertDoc.withoutLock( + project_id, + folder._id, + docName, + docLines, + source, + userId, + function(err, doc, isNewDoc) { + if (err != null) { + return callback(err) + } + return callback(null, doc, isNewDoc, newFolders, folder) + } + ) + }) + }), + + upsertFileWithPath: wrapWithLock({ + beforeLock(next) { + return function( + project_id, + elementPath, + fsPath, + linkedFileData, + userId, + callback + ) { + if (!SafePath.isCleanPath(elementPath)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + const fileName = path.basename(elementPath) + const folderPath = path.dirname(elementPath) + // create a new file + const fileArgs = { + name: fileName, + linkedFileData + } + return FileStoreHandler.uploadFileFromDisk( + project_id, + fileArgs, + fsPath, + function(err, fileStoreUrl, fileRef) { + if (err != null) { + return callback(err) + } + return next( + project_id, + folderPath, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + project_id, + folderPath, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) { + return self.mkdirp.withoutLock(project_id, folderPath, function( + err, + newFolders, + folder + ) { + if (err != null) { + return callback(err) + } + // this calls directly into the upsertFile main task (without the beforeLock part) + return self.upsertFile.mainTask( + project_id, + folder._id, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + function(err, newFile, isNewFile, existingFile) { + if (err != null) { + return callback(err) + } + return callback( + null, + newFile, + isNewFile, + existingFile, + newFolders, + folder + ) + } + ) + }) + } + }), + + deleteEntity: wrapWithLock(function( + project_id, + entity_id, + entityType, + userId, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ entity_id, entityType, project_id }, 'deleting project entity') + if (entityType == null) { + logger.err({ err: 'No entityType set', project_id, entity_id }) + return callback('No entityType set') + } + entityType = entityType.toLowerCase() + return ProjectEntityMongoUpdateHandler.deleteEntity( + project_id, + entity_id, + entityType, + function(error, entity, path, projectBeforeDeletion, newProject) { + if (error != null) { + return callback(error) + } + return self._cleanUpEntity( + projectBeforeDeletion, + newProject, + entity, + entityType, + path.fileSystem, + userId, + function(error) { + if (error != null) { + return callback(error) + } + return TpdsUpdateSender.deleteEntity( + { + project_id, + path: path.fileSystem, + project_name: projectBeforeDeletion.name + }, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, entity_id) + } + ) + } + ) + } + ) + }), + + deleteEntityWithPath: wrapWithLock((project_id, path, userId, callback) => + ProjectLocator.findElementByPath({ project_id, path }, function( + err, + element, + type + ) { + if (err != null) { + return callback(err) + } + if (element == null) { + return callback(new Errors.NotFoundError('project not found')) + } + return self.deleteEntity.withoutLock( + project_id, + element._id, + type, + userId, + callback + ) + }) + ), + + mkdirp: wrapWithLock(function(project_id, path, callback) { + if (callback == null) { + callback = function(err, newlyCreatedFolders, lastFolderInPath) {} + } + for (let folder of Array.from(path.split('/'))) { + if (folder.length > 0 && !SafePath.isCleanFilename(folder)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + } + return ProjectEntityMongoUpdateHandler.mkdirp( + project_id, + path, + { exactCaseMatch: false }, + callback + ) + }), + + mkdirpWithExactCase: wrapWithLock(function(project_id, path, callback) { + if (callback == null) { + callback = function(err, newlyCreatedFolders, lastFolderInPath) {} + } + for (let folder of Array.from(path.split('/'))) { + if (folder.length > 0 && !SafePath.isCleanFilename(folder)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + } + return ProjectEntityMongoUpdateHandler.mkdirp( + project_id, + path, + { exactCaseMatch: true }, + callback + ) + }), + + addFolder: wrapWithLock(function( + project_id, + parentFolder_id, + folderName, + callback + ) { + if (!SafePath.isCleanFilename(folderName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + return ProjectEntityMongoUpdateHandler.addFolder( + project_id, + parentFolder_id, + folderName, + callback + ) + }), + + moveEntity: wrapWithLock(function( + project_id, + entity_id, + destFolderId, + entityType, + userId, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { entityType, entity_id, project_id, destFolderId }, + 'moving entity' + ) + if (entityType == null) { + logger.err({ err: 'No entityType set', project_id, entity_id }) + return callback('No entityType set') + } + entityType = entityType.toLowerCase() + return ProjectEntityMongoUpdateHandler.moveEntity( + project_id, + entity_id, + destFolderId, + entityType, + function(err, project, startPath, endPath, rev, changes) { + if (err != null) { + return callback(err) + } + const projectHistoryId = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ) + TpdsUpdateSender.moveEntity({ + project_id, + project_name: project.name, + startPath, + endPath, + rev + }) + return DocumentUpdaterHandler.updateProjectStructure( + project_id, + projectHistoryId, + userId, + changes, + callback + ) + } + ) + }), + + renameEntity: wrapWithLock(function( + project_id, + entity_id, + entityType, + newName, + userId, + callback + ) { + if (!SafePath.isCleanFilename(newName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + logger.log({ entity_id, project_id }, `renaming ${entityType}`) + if (entityType == null) { + logger.err({ err: 'No entityType set', project_id, entity_id }) + return callback('No entityType set') + } + entityType = entityType.toLowerCase() + + return ProjectEntityMongoUpdateHandler.renameEntity( + project_id, + entity_id, + entityType, + newName, + function(err, project, startPath, endPath, rev, changes) { + if (err != null) { + return callback(err) + } + const projectHistoryId = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ) + TpdsUpdateSender.moveEntity({ + project_id, + project_name: project.name, + startPath, + endPath, + rev + }) + return DocumentUpdaterHandler.updateProjectStructure( + project_id, + projectHistoryId, + userId, + changes, + callback + ) + } + ) + }), + + // This doesn't directly update project structure but we need to take the lock + // to prevent anything else being queued before the resync update + resyncProjectHistory: wrapWithLock((project_id, callback) => + ProjectGetter.getProject( + project_id, + { rootFolder: true, overleaf: true }, + function(error, project) { + if (error != null) { + return callback(error) + } + + const projectHistoryId = __guard__( + __guard__( + project != null ? project.overleaf : undefined, + x1 => x1.history + ), + x => x.id + ) + if (projectHistoryId == null) { + error = new Errors.ProjectHistoryDisabledError( + `project history not enabled for ${project_id}` + ) + return callback(error) + } + + return ProjectEntityHandler.getAllEntitiesFromProject(project, function( + error, + docs, + files + ) { + if (error != null) { + return callback(error) + } + + docs = _.map(docs, doc => ({ + doc: doc.doc._id, + path: doc.path + })) + + files = _.map(files, file => ({ + file: file.file._id, + path: file.path, + url: FileStoreHandler._buildUrl(project_id, file.file._id) + })) + + return DocumentUpdaterHandler.resyncProjectHistory( + project_id, + projectHistoryId, + docs, + files, + callback + ) + }) + } + ) + ), + + _cleanUpEntity( + project, + newProject, + entity, + entityType, + path, + userId, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + return self._updateProjectStructureWithDeletedEntity( + project, + newProject, + entity, + entityType, + path, + userId, + function(error) { + if (error != null) { + return callback(error) + } + if (entityType.indexOf('file') !== -1) { + return self._cleanUpFile(project, entity, path, userId, callback) + } else if (entityType.indexOf('doc') !== -1) { + return self._cleanUpDoc(project, entity, path, userId, callback) + } else if (entityType.indexOf('folder') !== -1) { + return self._cleanUpFolder(project, entity, path, userId, callback) + } else { + return callback() + } + } + ) + }, + + // Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity + // methods both need to recursively iterate over the entities in folder. + // These are currently using separate implementations of the recursion. In + // future, these could be simplified using a common project entity iterator. + _updateProjectStructureWithDeletedEntity( + project, + newProject, + entity, + entityType, + entityPath, + userId, + callback + ) { + // compute the changes to the project structure + let changes + if (callback == null) { + callback = function(error) {} + } + if (entityType.indexOf('file') !== -1) { + changes = { oldFiles: [{ file: entity, path: entityPath }] } + } else if (entityType.indexOf('doc') !== -1) { + changes = { oldDocs: [{ doc: entity, path: entityPath }] } + } else if (entityType.indexOf('folder') !== -1) { + changes = { oldDocs: [], oldFiles: [] } + var _recurseFolder = function(folder, folderPath) { + for (let doc of Array.from(folder.docs)) { + changes.oldDocs.push({ doc, path: path.join(folderPath, doc.name) }) + } + for (let file of Array.from(folder.fileRefs)) { + changes.oldFiles.push({ + file, + path: path.join(folderPath, file.name) + }) + } + return Array.from(folder.folders).map(childFolder => + _recurseFolder(childFolder, path.join(folderPath, childFolder.name)) + ) + } + _recurseFolder(entity, entityPath) + } + // now send the project structure changes to the docupdater + changes.newProject = newProject + const project_id = project._id.toString() + const projectHistoryId = __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ) + return DocumentUpdaterHandler.updateProjectStructure( + project_id, + projectHistoryId, + userId, + changes, + callback + ) + }, + + _cleanUpDoc(project, doc, path, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + const project_id = project._id.toString() + const doc_id = doc._id.toString() + const unsetRootDocIfRequired = callback => { + if ( + project.rootDoc_id != null && + project.rootDoc_id.toString() === doc_id + ) { + return this.unsetRootDoc(project_id, callback) + } else { + return callback() + } + } + + return unsetRootDocIfRequired(function(error) { + if (error != null) { + return callback(error) + } + return ProjectEntityMongoUpdateHandler._insertDeletedDocReference( + project._id, + doc, + function(error) { + if (error != null) { + return callback(error) + } + return DocumentUpdaterHandler.deleteDoc(project_id, doc_id, function( + error + ) { + if (error != null) { + return callback(error) + } + return DocstoreManager.deleteDoc(project_id, doc_id, callback) + }) + } + ) + }) + }, + + _cleanUpFile(project, file, path, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectEntityMongoUpdateHandler._insertDeletedFileReference( + project._id, + file, + callback + ) + }, + + _cleanUpFolder(project, folder, folderPath, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + const jobs = [] + for (let doc of Array.from(folder.docs)) { + ;(function(doc) { + const docPath = path.join(folderPath, doc.name) + return jobs.push(callback => + self._cleanUpDoc(project, doc, docPath, userId, callback) + ) + })(doc) + } + + for (let file of Array.from(folder.fileRefs)) { + ;(function(file) { + const filePath = path.join(folderPath, file.name) + return jobs.push(callback => + self._cleanUpFile(project, file, filePath, userId, callback) + ) + })(file) + } + + for (let childFolder of Array.from(folder.folders)) { + ;(function(childFolder) { + folderPath = path.join(folderPath, childFolder.name) + return jobs.push(callback => + self._cleanUpFolder( + project, + childFolder, + folderPath, + userId, + callback + ) + ) + })(childFolder) + } + + return async.series(jobs, callback) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Project/ProjectGetter.js b/services/web/app/src/Features/Project/ProjectGetter.js new file mode 100644 index 0000000000..f013e119da --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectGetter.js @@ -0,0 +1,195 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectGetter +const mongojs = require('../../infrastructure/mongojs') +const metrics = require('metrics-sharelatex') +const { db } = mongojs +const { ObjectId } = mongojs +const async = require('async') +const { Project } = require('../../models/Project') +const logger = require('logger-sharelatex') +const LockManager = require('../../infrastructure/LockManager') + +module.exports = ProjectGetter = { + EXCLUDE_DEPTH: 8, + + getProjectWithoutDocLines(project_id, callback) { + if (callback == null) { + callback = function(error, project) {} + } + const excludes = {} + for ( + let i = 1, end = ProjectGetter.EXCLUDE_DEPTH, asc = end >= 1; + asc ? i <= end : i >= end; + asc ? i++ : i-- + ) { + excludes[`rootFolder${Array(i).join('.folders')}.docs.lines`] = 0 + } + return ProjectGetter.getProject(project_id, excludes, callback) + }, + + getProjectWithOnlyFolders(project_id, callback) { + if (callback == null) { + callback = function(error, project) {} + } + const excludes = {} + for ( + let i = 1, end = ProjectGetter.EXCLUDE_DEPTH, asc = end >= 1; + asc ? i <= end : i >= end; + asc ? i++ : i-- + ) { + excludes[`rootFolder${Array(i).join('.folders')}.docs`] = 0 + excludes[`rootFolder${Array(i).join('.folders')}.fileRefs`] = 0 + } + return ProjectGetter.getProject(project_id, excludes, callback) + }, + + getProject(project_id, projection, callback) { + if (typeof projection === 'function' && callback == null) { + callback = projection + projection = {} + } + if (project_id == null) { + return callback(new Error('no project_id provided')) + } + if (typeof projection !== 'object') { + return callback(new Error('projection is not an object')) + } + + if ( + (projection != null ? projection.rootFolder : undefined) || + Object.keys(projection).length === 0 + ) { + const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') + return LockManager.runWithLock( + ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE, + project_id, + cb => ProjectGetter.getProjectWithoutLock(project_id, projection, cb), + callback + ) + } else { + return ProjectGetter.getProjectWithoutLock( + project_id, + projection, + callback + ) + } + }, + + getProjectWithoutLock(project_id, projection, callback) { + let query + if (typeof projection === 'function' && callback == null) { + callback = projection + projection = {} + } + if (project_id == null) { + return callback(new Error('no project_id provided')) + } + if (typeof projection !== 'object') { + return callback(new Error('projection is not an object')) + } + + if (typeof project_id === 'string') { + query = { _id: ObjectId(project_id) } + } else if (project_id instanceof ObjectId) { + query = { _id: project_id } + } else if ( + (project_id != null ? project_id.toString().length : undefined) === 24 + ) { + // sometimes mongoose ids are hard to identify, this will catch them + query = { _id: ObjectId(project_id.toString()) } + } else { + const err = new Error('malformed get request') + logger.log( + { project_id, err, type: typeof project_id }, + 'malformed get request' + ) + return callback(err) + } + + return db.projects.find(query, projection, function(err, project) { + if (err != null) { + logger.err({ err, query, projection }, 'error getting project') + return callback(err) + } + return callback(null, project != null ? project[0] : undefined) + }) + }, + + getProjectIdByReadAndWriteToken(token, callback) { + if (callback == null) { + callback = function(err, project_id) {} + } + return Project.findOne( + { 'tokens.readAndWrite': token }, + { _id: 1 }, + function(err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback() + } + return callback(null, project._id) + } + ) + }, + + findAllUsersProjects(user_id, fields, callback) { + if (callback == null) { + callback = function(error, projects) { + if (projects == null) { + projects = { + owned: [], + readAndWrite: [], + readOnly: [], + tokenReadAndWrite: [], + tokenReadOnly: [] + } + } + } + } + const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') + return Project.find({ owner_ref: user_id }, fields, function( + error, + ownedProjects + ) { + if (error != null) { + return callback(error) + } + return CollaboratorsHandler.getProjectsUserIsMemberOf( + user_id, + fields, + function(error, projects) { + if (error != null) { + return callback(error) + } + const result = { + owned: ownedProjects || [], + readAndWrite: projects.readAndWrite || [], + readOnly: projects.readOnly || [], + tokenReadAndWrite: projects.tokenReadAndWrite || [], + tokenReadOnly: projects.tokenReadOnly || [] + } + return callback(null, result) + } + ) + }) + } +} +;['getProject', 'getProjectWithoutDocLines'].map(method => + metrics.timeAsyncMethod(ProjectGetter, method, 'mongo.ProjectGetter', logger) +) diff --git a/services/web/app/src/Features/Project/ProjectHelper.js b/services/web/app/src/Features/Project/ProjectHelper.js new file mode 100644 index 0000000000..35ea36b664 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectHelper.js @@ -0,0 +1,103 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectHelper +const ENGINE_TO_COMPILER_MAP = { + latex_dvipdf: 'latex', + pdflatex: 'pdflatex', + xelatex: 'xelatex', + lualatex: 'lualatex' +} + +module.exports = ProjectHelper = { + compilerFromV1Engine(engine) { + return ENGINE_TO_COMPILER_MAP[engine] + }, + + ensureNameIsUnique(nameList, name, suffixes, maxLength, callback) { + // create a set of all project names + if (suffixes == null) { + suffixes = [] + } + if (callback == null) { + callback = function(error, name, changed) {} + } + const allNames = new Set(nameList) + const isUnique = x => !allNames.has(x) + // check if the supplied name is already unique + if (isUnique(name)) { + return callback(null, name, false) + } + // the name already exists, try adding the user-supplied suffixes to generate a unique name + for (let suffix of Array.from(suffixes)) { + const candidateName = ProjectHelper._addSuffixToProjectName( + name, + suffix, + maxLength + ) + if (isUnique(candidateName)) { + return callback(null, candidateName, true) + } + } + // if there are no (more) suffixes, use a numeric one + const uniqueName = ProjectHelper._addNumericSuffixToProjectName( + name, + allNames, + maxLength + ) + if (uniqueName != null) { + return callback(null, uniqueName, true) + } else { + return callback( + new Error(`Failed to generate a unique name for: ${name}`) + ) + } + }, + + _addSuffixToProjectName(name, suffix, maxLength) { + // append the suffix and truncate the project title if needed + if (suffix == null) { + suffix = '' + } + const truncatedLength = maxLength - suffix.length + return name.substr(0, truncatedLength) + suffix + }, + + _addNumericSuffixToProjectName(name, allProjectNames, maxLength) { + const NUMERIC_SUFFIX_MATCH = / \((\d+)\)$/ + const suffixedName = function(basename, number) { + const suffix = ` (${number})` + return basename.substr(0, maxLength - suffix.length) + suffix + } + + const match = name.match(NUMERIC_SUFFIX_MATCH) + let basename = name + let n = 1 + const last = allProjectNames.size + n + + if (match != null) { + basename = name.replace(NUMERIC_SUFFIX_MATCH, '') + n = parseInt(match[1]) + } + + while (n <= last) { + const candidate = suffixedName(basename, n) + if (!allProjectNames.has(candidate)) { + return candidate + } + n += 1 + } + + return null + } +} diff --git a/services/web/app/src/Features/Project/ProjectHistoryHandler.js b/services/web/app/src/Features/Project/ProjectHistoryHandler.js new file mode 100644 index 0000000000..ed4099c1da --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectHistoryHandler.js @@ -0,0 +1,173 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectHistoryHandler +const { Project } = require('../../models/Project') +const ProjectDetailsHandler = require('./ProjectDetailsHandler') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const HistoryManager = require('../History/HistoryManager') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') + +module.exports = ProjectHistoryHandler = { + setHistoryId(project_id, history_id, callback) { + // reject invalid history ids + if (callback == null) { + callback = function(err) {} + } + if (!history_id || typeof history_id !== 'number') { + return callback(new Error('invalid history id')) + } + // use $exists:false to prevent overwriting any existing history id, atomically + return Project.update( + { _id: project_id, 'overleaf.history.id': { $exists: false } }, + { 'overleaf.history.id': history_id }, + function(err, result) { + if (err != null) { + return callback(err) + } + if ((result != null ? result.n : undefined) === 0) { + return callback(new Error('history exists')) + } + return callback() + } + ) + }, + + getHistoryId(project_id, callback) { + if (callback == null) { + callback = function(err, result) {} + } + return ProjectDetailsHandler.getDetails(project_id, function(err, project) { + if (err != null) { + return callback(err) + } // n.b. getDetails returns an error if the project doesn't exist + return callback( + null, + __guard__( + __guard__( + project != null ? project.overleaf : undefined, + x1 => x1.history + ), + x => x.id + ) + ) + }) + }, + + upgradeHistory(project_id, callback) { + // project must have an overleaf.history.id before allowing display of new history + if (callback == null) { + callback = function(err, result) {} + } + return Project.update( + { _id: project_id, 'overleaf.history.id': { $exists: true } }, + { + 'overleaf.history.display': true, + 'overleaf.history.upgradedAt': new Date() + }, + function(err, result) { + if (err != null) { + return callback(err) + } + // return an error if overleaf.history.id wasn't present + if ((result != null ? result.n : undefined) === 0) { + return callback(new Error('history not upgraded')) + } + return callback() + } + ) + }, + + downgradeHistory(project_id, callback) { + if (callback == null) { + callback = function(err, result) {} + } + return Project.update( + { _id: project_id, 'overleaf.history.upgradedAt': { $exists: true } }, + { + 'overleaf.history.display': false, + $unset: { 'overleaf.history.upgradedAt': 1 } + }, + function(err, result) { + if (err != null) { + return callback(err) + } + if ((result != null ? result.n : undefined) === 0) { + return callback(new Error('history not downgraded')) + } + return callback() + } + ) + }, + + ensureHistoryExistsForProject(project_id, callback) { + // We can only set a history id for a project that doesn't have one. The + // history id is cached in the project history service, and changing an + // existing value corrupts the history, leaving it in an irrecoverable + // state. Setting a history id when one wasn't present before is ok, + // because undefined history ids aren't cached. + if (callback == null) { + callback = function(err) {} + } + return ProjectHistoryHandler.getHistoryId(project_id, function( + err, + history_id + ) { + if (err != null) { + return callback(err) + } + if (history_id != null) { + return callback() + } // history already exists, success + return HistoryManager.initializeProject(function(err, history) { + if (err != null) { + return callback(err) + } + if (!(history != null ? history.overleaf_id : undefined)) { + return callback(new Error('failed to initialize history id')) + } + return ProjectHistoryHandler.setHistoryId( + project_id, + history.overleaf_id, + function(err) { + if (err != null) { + return callback(err) + } + return ProjectEntityUpdateHandler.resyncProjectHistory( + project_id, + function(err) { + if (err != null) { + return callback(err) + } + logger.log( + { project_id, history_id: history.overleaf_id }, + 'started syncing project with new history id' + ) + return HistoryManager.flushProject(project_id, callback) + } + ) + } + ) + }) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Project/ProjectLocator.js b/services/web/app/src/Features/Project/ProjectLocator.js new file mode 100644 index 0000000000..6c98565ffe --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectLocator.js @@ -0,0 +1,358 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-array-constructor, + no-return-assign, + no-undef, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS203: Remove `|| {}` from converted for-own loops + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectLocator +const { Project } = require('../../models/Project') +const ProjectGetter = require('./ProjectGetter') +const Errors = require('../Errors/Errors') +const _ = require('underscore') +const logger = require('logger-sharelatex') +const async = require('async') + +module.exports = ProjectLocator = { + findElement(options, _callback) { + if (_callback == null) { + _callback = function(err, element, path, parentFolder) {} + } + const callback = function(...args) { + _callback(...Array.from(args || [])) + return (_callback = function() {}) + } + + const { project, project_id, element_id, type } = options + const elementType = sanitizeTypeOfElement(type) + + let count = 0 + const endOfBranch = function() { + if (--count === 0) { + logger.warn( + `element ${element_id} could not be found for project ${project_id || + project._id}` + ) + return callback(new Errors.NotFoundError('entity not found')) + } + } + + var search = function(searchFolder, path) { + count++ + const element = _.find( + searchFolder[elementType], + el => (el != null ? el._id : undefined) + '' === element_id + '' + ) // need to ToString both id's for robustness + if ( + element == null && + searchFolder.folders != null && + searchFolder.folders.length !== 0 + ) { + _.each(searchFolder.folders, function(folder, index) { + if (folder == null) { + return + } + const newPath = {} + for (let key of Object.keys(path || {})) { + const value = path[key] + newPath[key] = value + } // make a value copy of the string + newPath.fileSystem += `/${folder.name}` + newPath.mongo += `.folders.${index}` + return search(folder, newPath) + }) + endOfBranch() + } else if (element != null) { + const elementPlaceInArray = getIndexOf( + searchFolder[elementType], + element_id + ) + path.fileSystem += `/${element.name}` + path.mongo += `.${elementType}.${elementPlaceInArray}` + return callback(null, element, path, searchFolder) + } else if (element == null) { + return endOfBranch() + } + } + + const path = { fileSystem: '', mongo: 'rootFolder.0' } + + const startSearch = function(project) { + if (element_id + '' === project.rootFolder[0]._id + '') { + return callback(null, project.rootFolder[0], path, null) + } else { + return search(project.rootFolder[0], path) + } + } + + if (project != null) { + return startSearch(project) + } else { + return ProjectGetter.getProject( + project_id, + { rootFolder: true, rootDoc_id: true }, + function(err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('project not found')) + } + return startSearch(project) + } + ) + } + }, + + findRootDoc(opts, callback) { + const getRootDoc = project => { + if (project.rootDoc_id != null) { + return this.findElement( + { project, element_id: project.rootDoc_id, type: 'docs' }, + function(error, ...args) { + if (error != null) { + if (error instanceof Errors.NotFoundError) { + return callback(null, null) + } else { + return callback(error) + } + } + return callback(null, ...Array.from(args)) + } + ) + } else { + return callback(null, null) + } + } + const { project, project_id } = opts + if (project != null) { + return getRootDoc(project) + } else { + return ProjectGetter.getProject( + project_id, + { rootFolder: true, rootDoc_id: true }, + function(err, project) { + if (err != null) { + logger.err({ err }, 'error getting project') + return callback(err) + } else { + return getRootDoc(project) + } + } + ) + } + }, + + findElementByPath(options, callback) { + if (callback == null) { + callback = function(err, foundEntity, type) {} + } + const { project, project_id, path, exactCaseMatch } = options + if (path == null) { + return new Error('no path provided for findElementByPath') + } + + if (project != null) { + return ProjectLocator._findElementByPathWithProject( + project, + path, + exactCaseMatch, + callback + ) + } else { + return ProjectGetter.getProject( + project_id, + { rootFolder: true, rootDoc_id: true }, + function(err, project) { + if (err != null) { + return callback(err) + } + return ProjectLocator._findElementByPathWithProject( + project, + path, + exactCaseMatch, + callback + ) + } + ) + } + }, + + _findElementByPathWithProject(project, needlePath, exactCaseMatch, callback) { + let matchFn + if (callback == null) { + callback = function(err, foundEntity, type) {} + } + if (exactCaseMatch) { + matchFn = (a, b) => a === b + } else { + matchFn = (a, b) => + (a != null ? a.toLowerCase() : undefined) === + (b != null ? b.toLowerCase() : undefined) + } + + var getParentFolder = function(haystackFolder, foldersList, level, cb) { + if (foldersList.length === 0) { + return cb(null, haystackFolder) + } + const needleFolderName = foldersList[level] + let found = false + for (let folder of Array.from(haystackFolder.folders)) { + if (matchFn(folder.name, needleFolderName)) { + found = true + if (level === foldersList.length - 1) { + return cb(null, folder) + } else { + return getParentFolder(folder, foldersList, level + 1, cb) + } + } + } + if (!found) { + return cb( + `not found project: ${ + project._id + } search path: ${needlePath}, folder ${ + foldersList[level] + } could not be found` + ) + } + } + + const getEntity = function(folder, entityName, cb) { + let result, type + if (entityName == null) { + return cb(null, folder, 'folder') + } + for (let file of Array.from(folder.fileRefs || [])) { + if (matchFn(file != null ? file.name : undefined, entityName)) { + result = file + type = 'file' + } + } + for (let doc of Array.from(folder.docs || [])) { + if (matchFn(doc != null ? doc.name : undefined, entityName)) { + result = doc + type = 'doc' + } + } + for (let childFolder of Array.from(folder.folders || [])) { + if ( + matchFn( + childFolder != null ? childFolder.name : undefined, + entityName + ) + ) { + result = childFolder + type = 'folder' + } + } + + if (result != null) { + return cb(null, result, type) + } else { + return cb( + `not found project: ${ + project._id + } search path: ${needlePath}, entity ${entityName} could not be found` + ) + } + } + + if (typeof err !== 'undefined' && err !== null) { + logger.err( + { err, project_id: project._id }, + 'error getting project for finding element' + ) + return callback(err) + } + if (project == null) { + return callback( + `project could not be found for finding a element ${project._id}` + ) + } + if (needlePath === '' || needlePath === '/') { + return callback(null, project.rootFolder[0], 'folder') + } + + if (needlePath.indexOf('/') === 0) { + needlePath = needlePath.substring(1) + } + const foldersList = needlePath.split('/') + const needleName = foldersList.pop() + const rootFolder = project.rootFolder[0] + + logger.log( + { project_id: project._id, path: needlePath, foldersList }, + 'looking for element by path' + ) + const jobs = new Array() + jobs.push(cb => getParentFolder(rootFolder, foldersList, 0, cb)) + jobs.push((folder, cb) => getEntity(folder, needleName, cb)) + return async.waterfall(jobs, callback) + }, + + findUsersProjectByName(user_id, projectName, callback) { + return ProjectGetter.findAllUsersProjects( + user_id, + 'name archived', + function(err, allProjects) { + if (typeof error !== 'undefined' && error !== null) { + return callback(error) + } + const { owned, readAndWrite } = allProjects + const projects = owned.concat(readAndWrite) + projectName = projectName.toLowerCase() + const project = _.find( + projects, + project => + project.name.toLowerCase() === projectName && + project.archived !== true + ) + logger.log( + { user_id, projectName, totalProjects: projects.length, project }, + 'looking for project by name' + ) + return callback(null, project) + } + ) + } +} + +var sanitizeTypeOfElement = function(elementType) { + const lastChar = elementType.slice(-1) + if (lastChar !== 's') { + elementType += 's' + } + if (elementType === 'files') { + elementType = 'fileRefs' + } + return elementType +} + +var getIndexOf = function(searchEntity, id) { + const { length } = searchEntity + let count = 0 + while (count < length) { + if ( + (searchEntity[count] != null ? searchEntity[count]._id : undefined) + + '' === + id + '' + ) { + return count + } + count++ + } +} diff --git a/services/web/app/src/Features/Project/ProjectOptionsHandler.js b/services/web/app/src/Features/Project/ProjectOptionsHandler.js new file mode 100644 index 0000000000..7898d47e9c --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectOptionsHandler.js @@ -0,0 +1,126 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { Project } = require('../../models/Project') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const settings = require('settings-sharelatex') + +const safeCompilers = ['xelatex', 'pdflatex', 'latex', 'lualatex'] + +module.exports = { + setCompiler(project_id, compiler, callback) { + if (callback == null) { + callback = function() {} + } + logger.log({ project_id, compiler }, 'setting the compiler') + compiler = compiler.toLowerCase() + if (!_.contains(safeCompilers, compiler)) { + return callback() + } + const conditions = { _id: project_id } + const update = { compiler } + return Project.update(conditions, update, {}, function(err) { + if (callback != null) { + return callback() + } + }) + }, + + setImageName(project_id, imageName, callback) { + if (callback == null) { + callback = function() {} + } + logger.log({ project_id, imageName }, 'setting the imageName') + imageName = imageName.toLowerCase() + if ( + !_.some( + settings.allowedImageNames, + allowed => imageName === allowed.imageName + ) + ) { + return callback() + } + const conditions = { _id: project_id } + const update = { imageName: settings.imageRoot + '/' + imageName } + return Project.update(conditions, update, {}, function(err) { + if (callback != null) { + return callback() + } + }) + }, + + setSpellCheckLanguage(project_id, languageCode, callback) { + if (callback == null) { + callback = function() {} + } + logger.log({ project_id, languageCode }, 'setting the spell check language') + let languageIsSafe = false + settings.languages.forEach(function(safeLang) { + if (safeLang.code === languageCode) { + return (languageIsSafe = true) + } + }) + + if (languageCode === '') { + languageIsSafe = true + } + + if (languageIsSafe) { + const conditions = { _id: project_id } + const update = { spellCheckLanguage: languageCode } + return Project.update(conditions, update, {}, err => callback()) + } else { + logger.err({ project_id, languageCode }, 'tryed to set unsafe language') + return callback() + } + }, + + setBrandVariationId(project_id, brandVariationId, callback) { + if (callback == null) { + callback = function() {} + } + logger.log( + { project_id, brandVariationId }, + 'setting the brand variation id' + ) + if (brandVariationId == null || brandVariationId === '') { + return callback() + } + const conditions = { _id: project_id } + const update = { brandVariationId } + return Project.update(conditions, update, {}, function(err) { + if (err != null) { + logger.err({ err }, 'error setting brandVariationId') + } + return callback() + }) + }, + + unsetBrandVariationId(project_id, callback) { + if (callback == null) { + callback = function() {} + } + logger.log({ project_id }, 'unsetting the brand variation id') + const conditions = { _id: project_id } + const update = { $unset: { brandVariationId: 1 } } + return Project.update(conditions, update, {}, function(err) { + if (err != null) { + logger.err({ err }, 'error unsetting brandVariationId') + return callback(err) + } + return callback() + }) + } +} diff --git a/services/web/app/src/Features/Project/ProjectRootDocManager.js b/services/web/app/src/Features/Project/ProjectRootDocManager.js new file mode 100644 index 0000000000..41ec4500ca --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectRootDocManager.js @@ -0,0 +1,309 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectRootDocManager +const ProjectEntityHandler = require('./ProjectEntityHandler') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') +const ProjectGetter = require('./ProjectGetter') +const DocumentHelper = require('../Documents/DocumentHelper') +const Path = require('path') +const fs = require('fs') +const async = require('async') +const globby = require('globby') +const _ = require('underscore') + +module.exports = ProjectRootDocManager = { + setRootDocAutomatically(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectEntityHandler.getAllDocs(project_id, function(error, docs) { + if (error != null) { + return callback(error) + } + + const jobs = _.map( + docs, + (doc, path) => + function(cb) { + if ( + /\.R?tex$/.test(Path.extname(path)) && + DocumentHelper.contentHasDocumentclass(doc.lines) + ) { + return cb(doc._id) + } else { + return cb(null) + } + } + ) + + return async.series(jobs, function(root_doc_id) { + if (root_doc_id != null) { + return ProjectEntityUpdateHandler.setRootDoc( + project_id, + root_doc_id, + callback + ) + } else { + return callback() + } + }) + }) + }, + + findRootDocFileFromDirectory(directoryPath, callback) { + if (callback == null) { + callback = function(error, path, content) {} + } + const filePathsPromise = globby(['**/*.{tex,Rtex}'], { + cwd: directoryPath, + followSymlinkedDirectories: false, + onlyFiles: true, + case: false + }) + + // the search order is such that we prefer files closer to the project root, then + // we go by file size in ascending order, because people often have a main + // file that just includes a bunch of other files; then we go by name, in + // order to be deterministic + filePathsPromise.then( + unsortedFiles => + ProjectRootDocManager._sortFileList( + unsortedFiles, + directoryPath, + function(err, files) { + if (err != null) { + return callback(err) + } + let doc = null + + return async.until( + () => doc != null || files.length === 0, + function(cb) { + const file = files.shift() + return fs.readFile( + Path.join(directoryPath, file), + 'utf8', + function(error, content) { + if (error != null) { + return cb(error) + } + content = (content || '').replace(/\r/g, '') + if (DocumentHelper.contentHasDocumentclass(content)) { + doc = { path: file, content } + } + return cb(null) + } + ) + }, + err => + callback( + err, + doc != null ? doc.path : undefined, + doc != null ? doc.content : undefined + ) + ) + } + ), + err => callback(err) + ) + + // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha + return null + }, + + setRootDocFromName(project_id, rootDocName, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectEntityHandler.getAllDocPathsFromProjectById( + project_id, + function(error, docPaths) { + let doc_id, path + if (error != null) { + return callback(error) + } + // strip off leading and trailing quotes from rootDocName + rootDocName = rootDocName.replace(/^\'|\'$/g, '') + // prepend a slash for the root folder if not present + if (rootDocName[0] !== '/') { + rootDocName = `/${rootDocName}` + } + // find the root doc from the filename + let root_doc_id = null + for (doc_id in docPaths) { + // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename" + path = docPaths[doc_id] + if (path === rootDocName) { + root_doc_id = doc_id + } + } + // try a basename match if there was no match + if (!root_doc_id) { + for (doc_id in docPaths) { + path = docPaths[doc_id] + if (Path.basename(path) === Path.basename(rootDocName)) { + root_doc_id = doc_id + } + } + } + // set the root doc id if we found a match + if (root_doc_id != null) { + return ProjectEntityUpdateHandler.setRootDoc( + project_id, + root_doc_id, + callback + ) + } else { + return callback() + } + } + ) + }, + + ensureRootDocumentIsSet(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectGetter.getProject(project_id, { rootDoc_id: 1 }, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + if (project == null) { + return callback(new Error('project not found')) + } + + if (project.rootDoc_id != null) { + return callback() + } else { + return ProjectRootDocManager.setRootDocAutomatically( + project_id, + callback + ) + } + }) + }, + + ensureRootDocumentIsValid(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return ProjectGetter.getProject(project_id, { rootDoc_id: 1 }, function( + error, + project + ) { + if (error != null) { + return callback(error) + } + if (project == null) { + return callback(new Error('project not found')) + } + + if (project.rootDoc_id != null) { + return ProjectEntityHandler.getAllDocPathsFromProjectById( + project_id, + function(error, docPaths) { + if (error != null) { + return callback(error) + } + let rootDocValid = false + for (let doc_id in docPaths) { + const _path = docPaths[doc_id] + if (doc_id === project.rootDoc_id) { + rootDocValid = true + } + } + if (rootDocValid) { + return callback() + } else { + return ProjectEntityUpdateHandler.setRootDoc( + project_id, + null, + () => + ProjectRootDocManager.setRootDocAutomatically( + project_id, + callback + ) + ) + } + } + ) + } else { + return ProjectRootDocManager.setRootDocAutomatically( + project_id, + callback + ) + } + }) + }, + + _sortFileList(listToSort, rootDirectory, callback) { + if (callback == null) { + callback = function(error, result) {} + } + return async.mapLimit( + listToSort, + 5, + (filePath, cb) => + fs.stat(Path.join(rootDirectory, filePath), function(err, stat) { + if (err != null) { + return cb(err) + } + return cb(null, { + size: stat.size, + path: filePath, + elements: filePath.split(Path.sep).length, + name: Path.basename(filePath) + }) + }), + function(err, files) { + if (err != null) { + return callback(err) + } + + return callback( + null, + _.map( + files.sort(ProjectRootDocManager._rootDocSort), + file => file.path + ) + ) + } + ) + }, + + _rootDocSort(a, b) { + // sort first by folder depth + if (a.elements !== b.elements) { + return a.elements - b.elements + } + // ensure main.tex is at the start of each folder + if (a.name === 'main.tex' && b.name !== 'main.tex') { + return -1 + } + if (a.name !== 'main.tex' && b.name === 'main.tex') { + return 1 + } + // prefer smaller files + if (a.size !== b.size) { + return a.size - b.size + } + // otherwise, use the full path name + return a.path.localeCompare(b.path) + } +} diff --git a/services/web/app/src/Features/Project/ProjectTokenGenerator.js b/services/web/app/src/Features/Project/ProjectTokenGenerator.js new file mode 100644 index 0000000000..c3f293fac8 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectTokenGenerator.js @@ -0,0 +1,107 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectTokenGenerator +const crypto = require('crypto') +const V1Api = require('../V1/V1Api') +const Async = require('async') +const logger = require('logger-sharelatex') + +// This module mirrors the token generation in Overleaf (`random_token.rb`), +// for the purposes of implementing token-based project access, like the +// 'unlisted-projects' feature in Overleaf + +module.exports = ProjectTokenGenerator = { + // (From Overleaf `random_token.rb`) + // Letters (not numbers! see generate_token) used in tokens. They're all + // consonants, to avoid embarassing words (I can't think of any that use only + // a y), and lower case "l" is omitted, because in many fonts it is + // indistinguishable from an upper case "I" (and sometimes even the number 1). + TOKEN_ALPHA: 'bcdfghjkmnpqrstvwxyz', + TOKEN_NUMERICS: '123456789', + + _randomString(length, alphabet) { + const result = crypto + .randomBytes(length) + .toJSON() + .data.map(b => alphabet[b % alphabet.length]) + .join('') + return result + }, + + // Generate a 12-char token with only characters from TOKEN_ALPHA, + // suitable for use as a read-only token for a project + readOnlyToken() { + return ProjectTokenGenerator._randomString( + 12, + ProjectTokenGenerator.TOKEN_ALPHA + ) + }, + + // Generate a longer token, with a numeric prefix, + // suitable for use as a read-and-write token for a project + readAndWriteToken() { + const numerics = ProjectTokenGenerator._randomString( + 10, + ProjectTokenGenerator.TOKEN_NUMERICS + ) + const token = ProjectTokenGenerator._randomString( + 12, + ProjectTokenGenerator.TOKEN_ALPHA + ) + const fullToken = `${numerics}${token}` + return { token: fullToken, numericPrefix: numerics } + }, + + generateUniqueReadOnlyToken(callback) { + if (callback == null) { + callback = function(err, token) {} + } + return Async.retry( + 10, + function(cb) { + const token = ProjectTokenGenerator.readOnlyToken() + logger.log({ token }, 'Generated read-only token') + return V1Api.request( + { + url: `/api/v1/sharelatex/docs/read_token/${token}/exists`, + json: true + }, + function(err, response, body) { + if (err != null) { + return cb(err) + } + if (response.statusCode !== 200) { + return cb( + new Error( + `non-200 response from v1 read-token-exists api: ${ + response.statusCode + }` + ) + ) + } + if (body.exists === true) { + return cb(new Error(`token already exists in v1: ${token}`)) + } else { + logger.log( + { token }, + 'Read-only token does not exist in v1, good to use' + ) + return cb(null, token) + } + } + ) + }, + callback + ) + } +} diff --git a/services/web/app/src/Features/Project/ProjectUpdateHandler.js b/services/web/app/src/Features/Project/ProjectUpdateHandler.js new file mode 100644 index 0000000000..facf7bfa04 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectUpdateHandler.js @@ -0,0 +1,67 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { Project } = require('../../models/Project') +const logger = require('logger-sharelatex') + +module.exports = { + markAsUpdated(projectId, lastUpdatedAt, lastUpdatedBy, callback) { + if (callback == null) { + callback = function() {} + } + if (lastUpdatedAt == null) { + lastUpdatedAt = new Date() + } + + const conditions = { + _id: projectId, + lastUpdated: { $lt: lastUpdatedAt } + } + + const update = { + lastUpdated: lastUpdatedAt || new Date().getTime(), + lastUpdatedBy + } + return Project.update(conditions, update, {}, callback) + }, + + markAsOpened(project_id, callback) { + const conditions = { _id: project_id } + const update = { lastOpened: Date.now() } + return Project.update(conditions, update, {}, function(err) { + if (callback != null) { + return callback() + } + }) + }, + + markAsInactive(project_id, callback) { + const conditions = { _id: project_id } + const update = { active: false } + return Project.update(conditions, update, {}, function(err) { + if (callback != null) { + return callback() + } + }) + }, + + markAsActive(project_id, callback) { + const conditions = { _id: project_id } + const update = { active: true } + return Project.update(conditions, update, {}, function(err) { + if (callback != null) { + return callback() + } + }) + } +} diff --git a/services/web/app/src/Features/Project/SafePath.js b/services/web/app/src/Features/Project/SafePath.js new file mode 100644 index 0000000000..a78c5e4604 --- /dev/null +++ b/services/web/app/src/Features/Project/SafePath.js @@ -0,0 +1,135 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// This file is shared between the frontend and server code of web, so that +// filename validation is the same in both implementations. +// Both copies must be kept in sync: +// app/coffee/Features/Project/SafePath.coffee +// public/coffee/ide/directives/SafePath.coffee + +const load = function() { + let SafePath + const BADCHAR_RX = new RegExp( + `\ +[\ +\\/\ +\\\\\ +\\*\ +\\u0000-\\u001F\ +\\u007F\ +\\u0080-\\u009F\ +\\uD800-\\uDFFF\ +]\ +`, + 'g' + ) + + const BADFILE_RX = new RegExp( + `\ +(^\\.$)\ +|(^\\.\\.$)\ +|(^\\s+)\ +|(\\s+$)\ +`, + 'g' + ) + + // Put a block on filenames which match javascript property names, as they + // can cause exceptions where the code puts filenames into a hash. This is a + // temporary workaround until the code in other places is made safe against + // property names. + // + // The list of property names is taken from + // ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype)) + const BLOCKEDFILE_RX = new RegExp(`\ +^(\ +prototype\ +|constructor\ +|toString\ +|toLocaleString\ +|valueOf\ +|hasOwnProperty\ +|isPrototypeOf\ +|propertyIsEnumerable\ +|__defineGetter__\ +|__lookupGetter__\ +|__defineSetter__\ +|__lookupSetter__\ +|__proto__\ +)$\ +`) + + const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary. + + return (SafePath = { + // convert any invalid characters to underscores in the given filename + clean(filename) { + filename = filename.replace(BADCHAR_RX, '_') + // for BADFILE_RX replace any matches with an equal number of underscores + filename = filename.replace(BADFILE_RX, match => + new Array(match.length + 1).join('_') + ) + // replace blocked filenames 'prototype' with '@prototype' + filename = filename.replace(BLOCKEDFILE_RX, '@$1') + return filename + }, + + // returns whether the filename is 'clean' (does not contain any invalid + // characters or reserved words) + isCleanFilename(filename) { + return ( + SafePath.isAllowedLength(filename) && + !BADCHAR_RX.test(filename) && + !BADFILE_RX.test(filename) + ) + }, + + isBlockedFilename(filename) { + return BLOCKEDFILE_RX.test(filename) + }, + + // returns whether a full path is 'clean' - e.g. is a full or relative path + // that points to a file, and each element passes the rules in 'isCleanFilename' + isCleanPath(path) { + const elements = path.split('/') + + const lastElementIsEmpty = elements[elements.length - 1].length === 0 + if (lastElementIsEmpty) { + return false + } + + for (let element of Array.from(elements)) { + if (element.length > 0 && !SafePath.isCleanFilename(element)) { + return false + } + } + + // check for a top-level reserved name + if (BLOCKEDFILE_RX.test(path.replace(/^\/?/, ''))) { + return false + } // remove leading slash if present + + return true + }, + + isAllowedLength(pathname) { + return pathname.length > 0 && pathname.length <= MAX_PATH + } + }) +} + +if (typeof define !== 'undefined' && define !== null) { + define([], load) +} else { + module.exports = load() +} diff --git a/services/web/app/src/Features/Publishers/PublishersGetter.js b/services/web/app/src/Features/Publishers/PublishersGetter.js new file mode 100644 index 0000000000..31679c7d0f --- /dev/null +++ b/services/web/app/src/Features/Publishers/PublishersGetter.js @@ -0,0 +1,32 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let PublishersGetter +const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler') +const UserMembershipEntityConfigs = require('../UserMembership/UserMembershipEntityConfigs') +const logger = require('logger-sharelatex') +const _ = require('underscore') + +module.exports = PublishersGetter = { + getManagedPublishers(user_id, callback) { + if (callback == null) { + callback = function(error, managedPublishers) {} + } + return UserMembershipsHandler.getEntitiesByUser( + UserMembershipEntityConfigs.publisher, + user_id, + (error, managedPublishers) => callback(error, managedPublishers) + ) + } +} diff --git a/services/web/app/src/Features/RealTimeProxy/RealTimeProxyRouter.js b/services/web/app/src/Features/RealTimeProxy/RealTimeProxyRouter.js new file mode 100644 index 0000000000..2952f210e8 --- /dev/null +++ b/services/web/app/src/Features/RealTimeProxy/RealTimeProxyRouter.js @@ -0,0 +1,35 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('settings-sharelatex') + +const httpProxy = require('http-proxy') +const proxy = httpProxy.createProxyServer({ + target: settings.apis.realTime.url +}) +const wsProxy = httpProxy.createProxyServer({ + target: settings.apis.realTime.url.replace('http://', 'ws://'), + ws: true +}) + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.all(/\/socket\.io\/.*/, (req, res, next) => + proxy.web(req, res, next) + ) + + return setTimeout(function() { + const Server = require('../../infrastructure/Server') + return Server.server.on('upgrade', (req, socket, head) => + wsProxy.ws(req, socket, head) + ) + }, 0) + } +} diff --git a/services/web/app/src/Features/Referal/ReferalAllocator.js b/services/web/app/src/Features/Referal/ReferalAllocator.js new file mode 100644 index 0000000000..2687b7e76d --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalAllocator.js @@ -0,0 +1,75 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ReferalAllocator +const _ = require('underscore') +const logger = require('logger-sharelatex') +const { User } = require('../../models/User') +const Settings = require('settings-sharelatex') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') + +module.exports = ReferalAllocator = { + allocate(referal_id, new_user_id, referal_source, referal_medium, callback) { + if (callback == null) { + callback = function() {} + } + if (referal_id == null) { + logger.log({ new_user_id }, 'no referal for user') + return callback(null) + } + + logger.log( + { referal_id, new_user_id, referal_source, referal_medium }, + 'allocating users referal' + ) + + const query = { referal_id: referal_id } + return User.findOne(query, function(error, user) { + if (error != null) { + return callback(error) + } + if (user == null || user._id == null) { + logger.log({ new_user_id, referal_id }, 'no user found for referal id') + return callback(null) + } + + if (referal_source === 'bonus') { + return User.update( + query, + { + $push: { + refered_users: new_user_id + }, + $inc: { + refered_user_count: 1 + } + }, + {}, + function(err) { + if (err != null) { + logger.err( + { err, referal_id, new_user_id }, + 'something went wrong allocating referal' + ) + return callback(err) + } + + return FeaturesUpdater.refreshFeatures(user._id, callback) + } + ) + } else { + return callback() + } + }) + } +} diff --git a/services/web/app/src/Features/Referal/ReferalConnect.js b/services/web/app/src/Features/Referal/ReferalConnect.js new file mode 100644 index 0000000000..ece3f95c1f --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalConnect.js @@ -0,0 +1,63 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +module.exports = { + use(req, res, next) { + if (req.query != null) { + if (req.query.referal != null) { + req.session.referal_id = req.query.referal + } else if (req.query.r != null) { + // Short hand for referal + req.session.referal_id = req.query.r + } else if (req.query.fb_ref != null) { + req.session.referal_id = req.query.fb_ref + } + + if (req.query.rm != null) { + // referal medium e.g. twitter, facebook, email + switch (req.query.rm) { + case 'fb': + req.session.referal_medium = 'facebook' + break + case 't': + req.session.referal_medium = 'twitter' + break + case 'gp': + req.session.referal_medium = 'google_plus' + break + case 'e': + req.session.referal_medium = 'email' + break + case 'd': + req.session.referal_medium = 'direct' + break + } + } + + if (req.query.rs != null) { + // referal source e.g. project share, bonus + switch (req.query.rs) { + case 'b': + req.session.referal_source = 'bonus' + break + case 'ps': + req.session.referal_source = 'public_share' + break + case 'ci': + req.session.referal_source = 'collaborator_invite' + break + } + } + } + + return next() + } +} diff --git a/services/web/app/src/Features/Referal/ReferalController.js b/services/web/app/src/Features/Referal/ReferalController.js new file mode 100644 index 0000000000..6648a7ea38 --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalController.js @@ -0,0 +1,31 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const logger = require('logger-sharelatex') +const ReferalHandler = require('./ReferalHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = { + bonus(req, res) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return ReferalHandler.getReferedUsers( + user_id, + (err, refered_users, refered_user_count) => + res.render('referal/bonus', { + title: 'bonus_please_recommend_us', + refered_users, + refered_user_count + }) + ) + } +} diff --git a/services/web/app/src/Features/Referal/ReferalFeatures.js b/services/web/app/src/Features/Referal/ReferalFeatures.js new file mode 100644 index 0000000000..14e316a685 --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalFeatures.js @@ -0,0 +1,67 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ReferalFeatures +const _ = require('underscore') +const logger = require('logger-sharelatex') +const { User } = require('../../models/User') +const Settings = require('settings-sharelatex') + +module.exports = ReferalFeatures = { + getBonusFeatures(user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + const query = { _id: user_id } + return User.findOne(query, function(error, user) { + if (error) { + return callback(error) + } + if (user == null) { + return callback(new Error(`user not found ${user_id} for assignBonus`)) + } + logger.log( + { user_id, refered_user_count: user.refered_user_count }, + 'assigning bonus' + ) + if (user.refered_user_count != null && user.refered_user_count > 0) { + const newFeatures = ReferalFeatures._calculateFeatures(user) + return callback(null, newFeatures) + } else { + return callback(null, {}) + } + }) + }, + + _calculateFeatures(user) { + const bonusLevel = ReferalFeatures._getBonusLevel(user) + return ( + (Settings.bonus_features != null + ? Settings.bonus_features[`${bonusLevel}`] + : undefined) || {} + ) + }, + + _getBonusLevel(user) { + let highestBonusLevel = 0 + _.each(_.keys(Settings.bonus_features), function(level) { + const levelIsLessThanUser = level <= user.refered_user_count + const levelIsMoreThanCurrentHighest = level >= highestBonusLevel + if (levelIsLessThanUser && levelIsMoreThanCurrentHighest) { + return (highestBonusLevel = level) + } + }) + return highestBonusLevel + } +} diff --git a/services/web/app/src/Features/Referal/ReferalHandler.js b/services/web/app/src/Features/Referal/ReferalHandler.js new file mode 100644 index 0000000000..d715c801a5 --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalHandler.js @@ -0,0 +1,23 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { User } = require('../../models/User') + +module.exports = { + getReferedUsers(user_id, callback) { + return User.findById(user_id, function(err, user) { + const refered_users = user.refered_users || [] + const refered_user_count = user.refered_user_count || refered_users.length + return callback(null, refered_users, refered_user_count) + }) + } +} diff --git a/services/web/app/src/Features/References/ReferencesController.js b/services/web/app/src/Features/References/ReferencesController.js new file mode 100644 index 0000000000..1dd4b1965b --- /dev/null +++ b/services/web/app/src/Features/References/ReferencesController.js @@ -0,0 +1,83 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ReferencesController +const logger = require('logger-sharelatex') +const ReferencesHandler = require('./ReferencesHandler') +const settings = require('settings-sharelatex') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') + +module.exports = ReferencesController = { + index(req, res) { + const projectId = req.params.Project_id + const { shouldBroadcast } = req.body + const { docIds } = req.body + if (!docIds || !(docIds instanceof Array)) { + logger.err( + { projectId, docIds }, + "docIds is not valid, should be either Array or String 'ALL'" + ) + return res.sendStatus(400) + } + logger.log({ projectId, docIds }, 'index references for project') + return ReferencesHandler.index(projectId, docIds, function(err, data) { + if (err != null) { + logger.err({ err, projectId }, 'error indexing all references') + return res.sendStatus(500) + } + return ReferencesController._handleIndexResponse( + req, + res, + projectId, + shouldBroadcast, + data + ) + }) + }, + + indexAll(req, res) { + const projectId = req.params.Project_id + const { shouldBroadcast } = req.body + logger.log({ projectId }, 'index all references for project') + return ReferencesHandler.indexAll(projectId, function(err, data) { + if (err != null) { + logger.err({ err, projectId }, 'error indexing all references') + return res.sendStatus(500) + } + return ReferencesController._handleIndexResponse( + req, + res, + projectId, + shouldBroadcast, + data + ) + }) + }, + + _handleIndexResponse(req, res, projectId, shouldBroadcast, data) { + if (data == null || data.keys == null) { + 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 + ) + } + return res.json(data) + } +} diff --git a/services/web/app/src/Features/References/ReferencesHandler.js b/services/web/app/src/Features/References/ReferencesHandler.js new file mode 100644 index 0000000000..47f4df6983 --- /dev/null +++ b/services/web/app/src/Features/References/ReferencesHandler.js @@ -0,0 +1,236 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ReferencesHandler +const logger = require('logger-sharelatex') +const request = require('request') +const settings = require('settings-sharelatex') +const ProjectGetter = require('../Project/ProjectGetter') +const UserGetter = require('../User/UserGetter') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const _ = require('underscore') +const Async = require('async') + +const oneMinInMs = 60 * 1000 +const fiveMinsInMs = oneMinInMs * 5 + +if ( + __guard__( + settings.apis != null ? settings.apis.references : undefined, + x => x.url + ) == null +) { + logger.log('references search not enabled') +} + +module.exports = ReferencesHandler = { + _buildDocUrl(projectId, docId) { + return `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/raw` + }, + + _buildFileUrl(projectId, fileId) { + return `${settings.apis.filestore.url}/project/${projectId}/file/${fileId}` + }, + + _findBibFileIds(project) { + const ids = [] + var _process = function(folder) { + _.each(folder.fileRefs || [], function(file) { + if ( + __guard__(file != null ? file.name : undefined, x1 => + x1.match(/^.*\.bib$/) + ) + ) { + return ids.push(file._id) + } + }) + return _.each(folder.folders || [], folder => _process(folder)) + } + _.each(project.rootFolder || [], rootFolder => _process(rootFolder)) + return ids + }, + + _findBibDocIds(project) { + const ids = [] + var _process = function(folder) { + _.each(folder.docs || [], function(doc) { + if ( + __guard__(doc != null ? doc.name : undefined, x1 => + x1.match(/^.*\.bib$/) + ) + ) { + return ids.push(doc._id) + } + }) + return _.each(folder.folders || [], folder => _process(folder)) + } + _.each(project.rootFolder || [], rootFolder => _process(rootFolder)) + return ids + }, + + _isFullIndex(project, callback) { + if (callback == null) { + callback = function(err, result) {} + } + return UserGetter.getUser(project.owner_ref, { features: true }, function( + err, + owner + ) { + if (err != null) { + return callback(err) + } + const features = owner != null ? owner.features : undefined + return callback( + null, + (features != null ? features.references : undefined) === true || + (features != null ? features.referencesSearch : undefined) === true + ) + }) + }, + + indexAll(projectId, callback) { + if (callback == null) { + callback = function(err, data) {} + } + return ProjectGetter.getProject( + projectId, + { rootFolder: true, owner_ref: 1 }, + function(err, project) { + if (err) { + logger.err({ err, projectId }, 'error finding project') + return callback(err) + } + logger.log({ projectId }, 'indexing all bib files in project') + const docIds = ReferencesHandler._findBibDocIds(project) + const fileIds = ReferencesHandler._findBibFileIds(project) + return ReferencesHandler._doIndexOperation( + projectId, + project, + docIds, + fileIds, + callback + ) + } + ) + }, + + index(projectId, docIds, callback) { + if (callback == null) { + callback = function(err, data) {} + } + return ProjectGetter.getProject( + projectId, + { rootFolder: true, owner_ref: 1 }, + function(err, project) { + if (err) { + logger.err({ err, projectId }, 'error finding project') + return callback(err) + } + return ReferencesHandler._doIndexOperation( + projectId, + project, + docIds, + [], + callback + ) + } + ) + }, + + _doIndexOperation(projectId, project, docIds, fileIds, callback) { + if ( + __guard__( + settings.apis != null ? settings.apis.references : undefined, + x1 => x1.url + ) == null + ) { + return callback() + } + return ReferencesHandler._isFullIndex(project, function(err, isFullIndex) { + if (err) { + logger.err( + { err, projectId }, + 'error checking whether to do full index' + ) + return callback(err) + } + logger.log( + { projectId, docIds }, + 'flushing docs to mongo before calling references service' + ) + return Async.series( + docIds.map(docId => cb => + DocumentUpdaterHandler.flushDocToMongo(projectId, docId, cb) + ), + function(err) { + // continue + if (err) { + logger.err( + { err, projectId, docIds }, + 'error flushing docs to mongo' + ) + return callback(err) + } + const bibDocUrls = docIds.map(docId => + ReferencesHandler._buildDocUrl(projectId, docId) + ) + const bibFileUrls = fileIds.map(fileId => + ReferencesHandler._buildFileUrl(projectId, fileId) + ) + const allUrls = bibDocUrls.concat(bibFileUrls) + logger.log( + { projectId, isFullIndex, docIds, bibDocUrls }, + 'sending request to references service' + ) + return request.post( + { + url: `${settings.apis.references.url}/project/${projectId}/index`, + json: { + docUrls: allUrls, + fullIndex: isFullIndex + } + }, + function(err, res, data) { + if (err) { + logger.err( + { err, projectId }, + 'error communicating with references api' + ) + return callback(err) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log({ projectId }, 'got keys from references api') + return callback(null, data) + } else { + err = new Error( + `references api responded with non-success code: ${ + res.statusCode + }` + ) + logger.log({ err, projectId }, 'error updating references') + return callback(err) + } + } + ) + } + ) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Security/LoginRateLimiter.js b/services/web/app/src/Features/Security/LoginRateLimiter.js new file mode 100644 index 0000000000..32c59232e1 --- /dev/null +++ b/services/web/app/src/Features/Security/LoginRateLimiter.js @@ -0,0 +1,36 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const RateLimiter = require('../../infrastructure/RateLimiter') + +const ONE_MIN = 60 +const ATTEMPT_LIMIT = 10 + +module.exports = { + processLoginRequest(email, callback) { + const opts = { + endpointName: 'login', + throttle: ATTEMPT_LIMIT, + timeInterval: ONE_MIN * 2, + subjectName: email + } + return RateLimiter.addCount(opts, (err, shouldAllow) => + callback(err, shouldAllow) + ) + }, + + recordSuccessfulLogin(email, callback) { + if (callback == null) { + callback = function() {} + } + return RateLimiter.clearRateLimit('login', email, callback) + } +} diff --git a/services/web/app/src/Features/Security/OneTimeTokenHandler.js b/services/web/app/src/Features/Security/OneTimeTokenHandler.js new file mode 100644 index 0000000000..be6c0692ed --- /dev/null +++ b/services/web/app/src/Features/Security/OneTimeTokenHandler.js @@ -0,0 +1,94 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Settings = require('settings-sharelatex') +const crypto = require('crypto') +const logger = require('logger-sharelatex') +const { db } = require('../../infrastructure/mongojs') +const Errors = require('../Errors/Errors') + +const ONE_HOUR_IN_S = 60 * 60 + +module.exports = { + getNewToken(use, data, options, callback) { + // options is optional + if (options == null) { + options = {} + } + if (callback == null) { + callback = function(error, data) {} + } + if (typeof options === 'function') { + callback = options + options = {} + } + const expiresIn = options.expiresIn || ONE_HOUR_IN_S + const createdAt = new Date() + const expiresAt = new Date(createdAt.getTime() + expiresIn * 1000) + const token = crypto.randomBytes(32).toString('hex') + logger.log( + { data, expiresIn, token_start: token.slice(0, 8) }, + `generating token for ${use}` + ) + return db.tokens.insert( + { + use, + token, + data, + createdAt, + expiresAt + }, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, token) + } + ) + }, + + getValueFromTokenAndExpire(use, token, callback) { + if (callback == null) { + callback = function(error, data) {} + } + logger.log( + { token_start: token.slice(0, 8) }, + `getting data from ${use} token` + ) + const now = new Date() + return db.tokens.findAndModify( + { + query: { + use, + token, + expiresAt: { $gt: now }, + usedAt: { $exists: false } + }, + update: { + $set: { + usedAt: now + } + } + }, + function(error, token) { + if (error != null) { + return callback(error) + } + if (token == null) { + return callback(new Errors.NotFoundError('no token found')) + } + return callback(null, token.data) + } + ) + } +} diff --git a/services/web/app/src/Features/Security/RateLimiterMiddleware.js b/services/web/app/src/Features/Security/RateLimiterMiddleware.js new file mode 100644 index 0000000000..2701bc9e8b --- /dev/null +++ b/services/web/app/src/Features/Security/RateLimiterMiddleware.js @@ -0,0 +1,64 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RateLimiterMiddleware +const RateLimiter = require('../../infrastructure/RateLimiter') +const logger = require('logger-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = RateLimiterMiddleware = { + /* + Do not allow more than opts.maxRequests from a single client in + opts.timeInterval. Pass an array of opts.params to segment this based on + parameters in the request URL, e.g.: + + app.get "/project/:project_id", RateLimiterMiddleware.rateLimit(endpointName: "open-editor", params: ["project_id"]) + + will rate limit each project_id separately. + + Unique clients are identified by user_id if logged in, and IP address if not. + */ + rateLimit(opts) { + return function(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) || req.ip + const params = (opts.params || []).map(p => req.params[p]) + params.push(user_id) + let subjectName = params.join(':') + if (opts.ipOnly) { + subjectName = req.ip + } + if (opts.endpointName == null) { + throw new Error('no endpointName provided') + } + const options = { + endpointName: opts.endpointName, + timeInterval: opts.timeInterval || 60, + subjectName, + throttle: opts.maxRequests || 6 + } + return RateLimiter.addCount(options, function(error, canContinue) { + if (error != null) { + return next(error) + } + if (canContinue) { + return next() + } else { + logger.warn(options, 'rate limit exceeded') + res.status(429) // Too many requests + res.write('Rate limit reached, please try again later') + return res.end() + } + }) + } + } +} diff --git a/services/web/app/src/Features/ServerAdmin/AdminController.js b/services/web/app/src/Features/ServerAdmin/AdminController.js new file mode 100644 index 0000000000..ed2cc65b6e --- /dev/null +++ b/services/web/app/src/Features/ServerAdmin/AdminController.js @@ -0,0 +1,193 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let AdminController +const metrics = require('metrics-sharelatex') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const { User } = require('../../models/User') +const { Project } = require('../../models/Project') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const Settings = require('settings-sharelatex') +const util = require('util') +const RecurlyWrapper = require('../Subscription/RecurlyWrapper') +const SubscriptionHandler = require('../Subscription/SubscriptionHandler') +const projectEntityHandler = require('../Project/ProjectEntityHandler') +const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const SystemMessageManager = require('../SystemMessages/SystemMessageManager') + +const oneMinInMs = 60 * 1000 + +var updateOpenConnetionsMetrics = function() { + metrics.gauge( + 'open_connections.socketio', + __guard__( + __guard__( + __guard__(require('../../infrastructure/Server').io, x2 => x2.sockets), + x1 => x1.clients() + ), + x => x.length + ) + ) + metrics.gauge( + 'open_connections.http', + _.size(__guard__(require('http').globalAgent, x3 => x3.sockets)) + ) + metrics.gauge( + 'open_connections.https', + _.size(__guard__(require('https').globalAgent, x4 => x4.sockets)) + ) + return setTimeout(updateOpenConnetionsMetrics, oneMinInMs) +} + +setTimeout(updateOpenConnetionsMetrics, oneMinInMs) + +module.exports = AdminController = { + index: (req, res, next) => { + let agents, url + let agent + const http = require('http') + const openSockets = {} + const object = require('http').globalAgent.sockets + for (url in object) { + agents = object[url] + openSockets[`http://${url}`] = (() => { + const result = [] + for (agent of Array.from(agents)) { + result.push(agent._httpMessage.path) + } + return result + })() + } + const object1 = require('https').globalAgent.sockets + for (url in object1) { + agents = object1[url] + openSockets[`https://${url}`] = (() => { + const result1 = [] + for (agent of Array.from(agents)) { + result1.push(agent._httpMessage.path) + } + return result1 + })() + } + + return SystemMessageManager.getMessagesFromDB(function( + error, + systemMessages + ) { + if (error != null) { + return next(error) + } + return res.render('admin/index', { + title: 'System Admin', + openSockets, + systemMessages + }) + }) + }, + + registerNewUser(req, res, next) { + return res.render('admin/register') + }, + + dissconectAllUsers: (req, res) => { + logger.warn('disconecting everyone') + EditorRealTimeController.emitToAll( + 'forceDisconnect', + 'Sorry, we are performing a quick update to the editor and need to close it down. Please refresh the page to continue.' + ) + return res.sendStatus(200) + }, + + closeEditor(req, res) { + logger.warn('closing editor') + Settings.editorIsOpen = req.body.isOpen + return res.sendStatus(200) + }, + + writeAllToMongo(req, res) { + logger.log('writing all docs to mongo') + Settings.mongo.writeAll = true + return DocumentUpdaterHandler.flushAllDocsToMongo(function() { + logger.log('all docs have been saved to mongo') + return res.send() + }) + }, + + syncUserToSubscription(req, res) { + const { user_id, subscription_id } = req.body + return RecurlyWrapper.getSubscription( + subscription_id, + {}, + (err, subscription) => + User.findById(user_id, (err, user) => + SubscriptionHandler.syncSubscriptionToUser( + subscription, + user._id, + function(err) { + logger.log( + { user_id, subscription_id }, + 'linked account to subscription' + ) + return res.send() + } + ) + ) + ) + }, + + flushProjectToTpds(req, res) { + return projectEntityHandler.flushProjectToThirdPartyDataStore( + req.body.project_id, + err => res.sendStatus(200) + ) + }, + + pollDropboxForUser(req, res) { + const { user_id } = req.body + return TpdsUpdateSender.pollDropboxForUser(user_id, () => + res.sendStatus(200) + ) + }, + + createMessage(req, res, next) { + return SystemMessageManager.createMessage(req.body.content, function( + error + ) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + }, + + clearMessages(req, res, next) { + return SystemMessageManager.clearMessages(function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Spelling/SpellingController.js b/services/web/app/src/Features/Spelling/SpellingController.js new file mode 100644 index 0000000000..3f30ef4b20 --- /dev/null +++ b/services/web/app/src/Features/Spelling/SpellingController.js @@ -0,0 +1,40 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SpellingController +const request = require('request') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') + +const TEN_SECONDS = 1000 * 10 + +module.exports = SpellingController = { + proxyRequestToSpellingApi(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + let url = req.url.slice('/spelling'.length) + url = `/user/${user_id}${url}` + req.headers['Host'] = Settings.apis.spelling.host + return request({ + url: Settings.apis.spelling.url + url, + method: req.method, + headers: req.headers, + json: req.body, + timeout: TEN_SECONDS + }) + .on('error', function(error) { + logger.error({ err: error }, 'Spelling API error') + return res.status(500).end() + }) + .pipe(res) + } +} diff --git a/services/web/app/src/Features/StaticPages/HomeController.js b/services/web/app/src/Features/StaticPages/HomeController.js new file mode 100644 index 0000000000..c644de1534 --- /dev/null +++ b/services/web/app/src/Features/StaticPages/HomeController.js @@ -0,0 +1,84 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-path-concat, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HomeController +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') +const _ = require('underscore') +const Features = require('../../infrastructure/Features') + +const Path = require('path') +const fs = require('fs') + +const ErrorController = require('../Errors/ErrorController') +const AuthenticationController = require('../Authentication/AuthenticationController') + +const slHomepageExists = fs.existsSync( + Path.resolve(__dirname + '/../../../views/external/home/sl.pug') +) +const v2HomepageExists = fs.existsSync( + Path.resolve(__dirname + '/../../../views/external/home/v2.pug') +) + +module.exports = HomeController = { + index(req, res) { + if (AuthenticationController.isUserLoggedIn(req)) { + if (req.query.scribtex_path != null) { + return res.redirect(`/project?scribtex_path=${req.query.scribtex_path}`) + } else { + return res.redirect('/project') + } + } else { + return HomeController.home(req, res) + } + }, + + home(req, res, next) { + if ( + Features.hasFeature('homepage') && + !Settings.overleaf && + slHomepageExists + ) { + return res.render('external/home/sl') + } else if ( + Features.hasFeature('homepage') && + Settings.overleaf && + v2HomepageExists + ) { + return res.render('external/home/v2') + } else { + return res.redirect('/login') + } + }, + + externalPage(page, title) { + return function(req, res, next) { + if (next == null) { + next = function(error) {} + } + const path = Path.resolve( + __dirname + `/../../../views/external/${page}.pug` + ) + return fs.exists(path, function(exists) { + // No error in this callback - old method in Node.js! + if (exists) { + return res.render(`external/${page}.pug`, { title }) + } else { + return ErrorController.notFound(req, res, next) + } + }) + } + } +} diff --git a/services/web/app/src/Features/StaticPages/StaticPageHelpers.js b/services/web/app/src/Features/StaticPages/StaticPageHelpers.js new file mode 100644 index 0000000000..049f8418c7 --- /dev/null +++ b/services/web/app/src/Features/StaticPages/StaticPageHelpers.js @@ -0,0 +1,31 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const extensionsToProxy = [ + '.png', + '.xml', + '.jpeg', + '.json', + '.zip', + '.eps', + '.gif', + '.jpg' +] +const _ = require('underscore') + +module.exports = { + shouldProxy(url) { + const shouldProxy = _.find( + extensionsToProxy, + extension => url.indexOf(extension) !== -1 + ) + return shouldProxy + } +} diff --git a/services/web/app/src/Features/StaticPages/StaticPagesRouter.js b/services/web/app/src/Features/StaticPages/StaticPagesRouter.js new file mode 100644 index 0000000000..7c3ff4f2e2 --- /dev/null +++ b/services/web/app/src/Features/StaticPages/StaticPagesRouter.js @@ -0,0 +1,60 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const HomeController = require('./HomeController') +const UniversityController = require('./UniversityController') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.get('/', HomeController.index) + webRouter.get('/home', HomeController.home) + + webRouter.get( + '/tos', + HomeController.externalPage('tos', 'Terms of Service') + ) + webRouter.get('/about', HomeController.externalPage('about', 'About Us')) + + webRouter.get( + '/security', + HomeController.externalPage('security', 'Security') + ) + webRouter.get( + '/privacy_policy', + HomeController.externalPage('privacy', 'Privacy Policy') + ) + webRouter.get( + '/planned_maintenance', + HomeController.externalPage('planned_maintenance', 'Planned Maintenance') + ) + webRouter.get( + '/style', + HomeController.externalPage('style_guide', 'Style Guide') + ) + webRouter.get( + '/ol-style', + HomeController.externalPage('ol_style_guide', 'Overleaf Style Guide') + ) + webRouter.get('/jobs', HomeController.externalPage('jobs', 'Jobs')) + + webRouter.get( + '/track-changes-and-comments-in-latex', + HomeController.externalPage('review-features-page', 'Review features') + ) + + webRouter.get( + '/dropbox', + HomeController.externalPage('dropbox', 'Dropbox and ShareLaTeX') + ) + + webRouter.get('/university', UniversityController.getIndexPage) + return webRouter.get('/university/*', UniversityController.getPage) + } +} diff --git a/services/web/app/src/Features/StaticPages/UniversityController.js b/services/web/app/src/Features/StaticPages/UniversityController.js new file mode 100644 index 0000000000..c6ca05da91 --- /dev/null +++ b/services/web/app/src/Features/StaticPages/UniversityController.js @@ -0,0 +1,28 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UniversityController +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') +const sixpack = require('../../infrastructure/Sixpack') + +module.exports = UniversityController = { + getPage(req, res, next) { + const url = + req.url != null ? req.url.toLowerCase().replace('.html', '') : undefined + return res.redirect(`/i${url}`) + }, + + getIndexPage(req, res) { + return res.redirect('/i/university') + } +} diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.js b/services/web/app/src/Features/Subscription/FeaturesUpdater.js new file mode 100644 index 0000000000..00f1bfc0db --- /dev/null +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.js @@ -0,0 +1,216 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FeaturesUpdater +const async = require('async') +const PlansLocator = require('./PlansLocator') +const _ = require('underscore') +const SubscriptionLocator = require('./SubscriptionLocator') +const UserFeaturesUpdater = require('./UserFeaturesUpdater') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const ReferalFeatures = require('../Referal/ReferalFeatures') +const V1SubscriptionManager = require('./V1SubscriptionManager') +const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures') + +const oneMonthInSeconds = 60 * 60 * 24 * 30 + +module.exports = FeaturesUpdater = { + refreshFeatures(user_id, notifyV1, callback) { + if (notifyV1 == null) { + notifyV1 = true + } + if (callback == null) { + callback = function(error, features, featuresChanged) {} + } + if (typeof notifyV1 === 'function') { + callback = notifyV1 + notifyV1 = true + } + + if (notifyV1) { + V1SubscriptionManager.notifyV1OfFeaturesChange(user_id, function(error) { + if (error != null) { + return logger.err( + { err: error, user_id }, + 'error notifying v1 about updated features' + ) + } + }) + } + + const jobs = { + individualFeatures(cb) { + return FeaturesUpdater._getIndividualFeatures(user_id, cb) + }, + groupFeatureSets(cb) { + return FeaturesUpdater._getGroupFeatureSets(user_id, cb) + }, + institutionFeatures(cb) { + return InstitutionsFeatures.getInstitutionsFeatures(user_id, cb) + }, + v1Features(cb) { + return FeaturesUpdater._getV1Features(user_id, cb) + }, + bonusFeatures(cb) { + return ReferalFeatures.getBonusFeatures(user_id, cb) + } + } + return async.series(jobs, function(err, results) { + if (err != null) { + logger.err( + { err, user_id }, + 'error getting subscription or group for refreshFeatures' + ) + return callback(err) + } + + const { + individualFeatures, + groupFeatureSets, + institutionFeatures, + v1Features, + bonusFeatures + } = results + logger.log( + { + user_id, + individualFeatures, + groupFeatureSets, + institutionFeatures, + v1Features, + bonusFeatures + }, + 'merging user features' + ) + const featureSets = groupFeatureSets.concat([ + individualFeatures, + institutionFeatures, + v1Features, + bonusFeatures + ]) + const features = _.reduce( + featureSets, + FeaturesUpdater._mergeFeatures, + Settings.defaultFeatures + ) + + logger.log({ user_id, features }, 'updating user features') + return UserFeaturesUpdater.updateFeatures(user_id, features, callback) + }) + }, + + _getIndividualFeatures(user_id, callback) { + if (callback == null) { + callback = function(error, features) {} + } + return SubscriptionLocator.getUsersSubscription(user_id, (err, sub) => + callback(err, FeaturesUpdater._subscriptionToFeatures(sub)) + ) + }, + + _getGroupFeatureSets(user_id, callback) { + if (callback == null) { + callback = function(error, featureSets) {} + } + return SubscriptionLocator.getGroupSubscriptionsMemberOf( + user_id, + (err, subs) => + callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures)) + ) + }, + + _getV1Features(user_id, callback) { + if (callback == null) { + callback = function(error, features) {} + } + return V1SubscriptionManager.getPlanCodeFromV1(user_id, function( + err, + planCode, + v1Id + ) { + if (err != null) { + if ((err != null ? err.name : undefined) === 'NotFoundError') { + return callback(null, []) + } + return callback(err) + } + + return callback( + err, + FeaturesUpdater._mergeFeatures( + V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {}, + FeaturesUpdater._planCodeToFeatures(planCode) + ) + ) + }) + }, + + _mergeFeatures(featuresA, featuresB) { + const features = Object.assign({}, featuresA) + for (let key in featuresB) { + // Special merging logic for non-boolean features + const value = featuresB[key] + if (key === 'compileGroup') { + if ( + features['compileGroup'] === 'priority' || + featuresB['compileGroup'] === 'priority' + ) { + features['compileGroup'] = 'priority' + } else { + features['compileGroup'] = 'standard' + } + } else if (key === 'collaborators') { + if ( + features['collaborators'] === -1 || + featuresB['collaborators'] === -1 + ) { + features['collaborators'] = -1 + } else { + features['collaborators'] = Math.max( + features['collaborators'] || 0, + featuresB['collaborators'] || 0 + ) + } + } else if (key === 'compileTimeout') { + features['compileTimeout'] = Math.max( + features['compileTimeout'] || 0, + featuresB['compileTimeout'] || 0 + ) + } else { + // Boolean keys, true is better + features[key] = features[key] || featuresB[key] + } + } + return features + }, + + _subscriptionToFeatures(subscription) { + return FeaturesUpdater._planCodeToFeatures( + subscription != null ? subscription.planCode : undefined + ) + }, + + _planCodeToFeatures(planCode) { + if (planCode == null) { + return {} + } + const plan = PlansLocator.findLocalPlanInSettings(planCode) + if (plan == null) { + return {} + } else { + return plan.features + } + } +} diff --git a/services/web/app/src/Features/Subscription/GroupPlansData.js b/services/web/app/src/Features/Subscription/GroupPlansData.js new file mode 100644 index 0000000000..eb92e0ce2f --- /dev/null +++ b/services/web/app/src/Features/Subscription/GroupPlansData.js @@ -0,0 +1,61 @@ +/* eslint-disable + camelcase, + max-len, + no-path-concat, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Settings = require('settings-sharelatex') +const fs = require('fs') + +// The groups.json file encodes the various group plan options we provide, and +// is used in the app the render the appropriate dialog in the plans page, and +// to generate the appropriate entries in the Settings.plans array. +// It is also used by scripts/recurly/sync_recurly.rb, which will make sure +// Recurly has a plan configured for all the groups, and that the prices are +// up to date with the data in groups.json. +const data = fs.readFileSync( + __dirname + '/../../../templates/plans/groups.json' +) +const groups = JSON.parse(data.toString()) + +const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1) + +// With group accounts in Recurly, we end up with a lot of plans to manage. +// Rather than hand coding them in the settings file, and then needing to keep +// that data in sync with the data in groups.json, we can auto generate the +// group plan entries and append them to Settings.plans at boot time. This is not +// a particularly clean pattern, since it's a little surprising that settings +// are modified at boot-time, but I think it's a better option than trying to +// keep two sources of data in sync. +for (let usage in groups) { + const plan_data = groups[usage] + for (let plan_code in plan_data) { + const currency_data = plan_data[plan_code] + for (let currency in currency_data) { + const price_data = currency_data[currency] + for (let size in price_data) { + const price = price_data[size] + Settings.plans.push({ + planCode: `group_${plan_code}_${size}_${usage}`, + name: `${Settings.appName} ${capitalize( + plan_code + )} - Group Account (${size} licenses) - ${capitalize(usage)}`, + hideFromUsers: true, + annual: true, + features: Settings.features[plan_code], + groupPlan: true, + membersLimit: parseInt(size) + }) + } + } + } +} + +module.exports = groups diff --git a/services/web/app/src/Features/Subscription/LimitationsManager.js b/services/web/app/src/Features/Subscription/LimitationsManager.js new file mode 100644 index 0000000000..b7bceba911 --- /dev/null +++ b/services/web/app/src/Features/Subscription/LimitationsManager.js @@ -0,0 +1,258 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LimitationsManager +const logger = require('logger-sharelatex') +const ProjectGetter = require('../Project/ProjectGetter') +const UserGetter = require('../User/UserGetter') +const SubscriptionLocator = require('./SubscriptionLocator') +const Settings = require('settings-sharelatex') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsInvitesHandler = require('../Collaborators/CollaboratorsInviteHandler') +const V1SubscriptionManager = require('./V1SubscriptionManager') + +module.exports = LimitationsManager = { + allowedNumberOfCollaboratorsInProject(project_id, callback) { + return ProjectGetter.getProject( + project_id, + { owner_ref: true }, + (error, project) => { + if (error != null) { + return callback(error) + } + return this.allowedNumberOfCollaboratorsForUser( + project.owner_ref, + callback + ) + } + ) + }, + + allowedNumberOfCollaboratorsForUser(user_id, callback) { + return UserGetter.getUser(user_id, { features: 1 }, function(error, user) { + if (error != null) { + return callback(error) + } + if (user.features != null && user.features.collaborators != null) { + return callback(null, user.features.collaborators) + } else { + return callback(null, Settings.defaultFeatures.collaborators) + } + }) + }, + + canAddXCollaborators(project_id, x_collaborators, callback) { + if (callback == null) { + callback = function(error, allowed) {} + } + return this.allowedNumberOfCollaboratorsInProject( + project_id, + (error, allowed_number) => { + if (error != null) { + return callback(error) + } + return CollaboratorsHandler.getInvitedCollaboratorCount( + project_id, + (error, current_number) => { + if (error != null) { + return callback(error) + } + return CollaboratorsInvitesHandler.getInviteCount( + project_id, + (error, invite_count) => { + if (error != null) { + return callback(error) + } + if ( + current_number + invite_count + x_collaborators <= + allowed_number || + allowed_number < 0 + ) { + return callback(null, true) + } else { + return callback(null, false) + } + } + ) + } + ) + } + ) + }, + + hasPaidSubscription(user, callback) { + if (callback == null) { + callback = function(err, hasSubscriptionOrIsMember) {} + } + return this.userHasV2Subscription( + user, + (err, hasSubscription, subscription) => { + if (err != null) { + return callback(err) + } + return this.userIsMemberOfGroupSubscription(user, (err, isMember) => { + if (err != null) { + return callback(err) + } + return this.userHasV1Subscription(user, (err, hasV1Subscription) => { + if (err != null) { + return callback(err) + } + logger.log( + { + user_id: user._id, + isMember, + hasSubscription, + hasV1Subscription + }, + 'checking if user has subscription or is group member' + ) + return callback( + err, + isMember || hasSubscription || hasV1Subscription, + subscription + ) + }) + }) + } + ) + }, + + // alias for backward-compatibility with modules. Use `haspaidsubscription` instead + userHasSubscriptionOrIsGroupMember(user, callback) { + return this.hasPaidSubscription(user, callback) + }, + + userHasV2Subscription(user, callback) { + if (callback == null) { + callback = function(err, hasSubscription, subscription) {} + } + logger.log({ user_id: user._id }, 'checking if user has subscription') + return SubscriptionLocator.getUsersSubscription(user._id, function( + err, + subscription + ) { + if (err != null) { + return callback(err) + } + const hasValidSubscription = + subscription != null && + (subscription.recurlySubscription_id != null || + (subscription != null ? subscription.customAccount : undefined) === + true) + logger.log( + { user, hasValidSubscription, subscription }, + 'checking if user has subscription' + ) + return callback(err, hasValidSubscription, subscription) + }) + }, + + userHasV1OrV2Subscription(user, callback) { + if (callback == null) { + callback = function(err, hasSubscription) {} + } + return this.userHasV2Subscription(user, (err, hasV2Subscription) => { + if (err != null) { + return callback(err) + } + if (hasV2Subscription) { + return callback(null, true) + } + return this.userHasV1Subscription(user, (err, hasV1Subscription) => { + if (err != null) { + return callback(err) + } + if (hasV1Subscription) { + return callback(null, true) + } + return callback(null, false) + }) + }) + }, + + userIsMemberOfGroupSubscription(user, callback) { + if (callback == null) { + callback = function(error, isMember, subscriptions) {} + } + logger.log( + { user_id: user._id }, + 'checking is user is member of subscription groups' + ) + return SubscriptionLocator.getMemberSubscriptions(user._id, function( + err, + subscriptions + ) { + if (subscriptions == null) { + subscriptions = [] + } + if (err != null) { + return callback(err) + } + return callback(err, subscriptions.length > 0, subscriptions) + }) + }, + + userHasV1Subscription(user, callback) { + if (callback == null) { + callback = function(error, hasV1Subscription) {} + } + return V1SubscriptionManager.getSubscriptionsFromV1(user._id, function( + err, + v1Subscription + ) { + logger.log( + { user_id: user._id, v1Subscription }, + '[userHasV1Subscription]' + ) + return callback( + err, + !!(v1Subscription != null ? v1Subscription.has_subscription : undefined) + ) + }) + }, + + teamHasReachedMemberLimit(subscription) { + const currentTotal = + (subscription.member_ids || []).length + + (subscription.teamInvites || []).length + + (subscription.invited_emails || []).length + + return currentTotal >= subscription.membersLimit + }, + + hasGroupMembersLimitReached(subscriptionId, callback) { + if (callback == null) { + callback = function(err, limitReached, subscription) {} + } + return SubscriptionLocator.getSubscription(subscriptionId, function( + err, + subscription + ) { + if (err != null) { + logger.err({ err, subscriptionId }, 'error getting subscription') + return callback(err) + } + if (subscription == null) { + logger.err({ subscriptionId }, 'no subscription found') + return callback('no subscription found') + } + + const limitReached = LimitationsManager.teamHasReachedMemberLimit( + subscription + ) + return callback(err, limitReached, subscription) + }) + } +} diff --git a/services/web/app/src/Features/Subscription/PlansLocator.js b/services/web/app/src/Features/Subscription/PlansLocator.js new file mode 100644 index 0000000000..ca0bd0dc9c --- /dev/null +++ b/services/web/app/src/Features/Subscription/PlansLocator.js @@ -0,0 +1,19 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Settings = require('settings-sharelatex') + +module.exports = { + findLocalPlanInSettings(planCode) { + for (let plan of Array.from(Settings.plans)) { + if (plan.planCode === planCode) { + return plan + } + } + return null + } +} diff --git a/services/web/app/src/Features/Subscription/RecurlyWrapper.js b/services/web/app/src/Features/Subscription/RecurlyWrapper.js new file mode 100644 index 0000000000..df5ce05ea2 --- /dev/null +++ b/services/web/app/src/Features/Subscription/RecurlyWrapper.js @@ -0,0 +1,979 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, + node/no-deprecated-api, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RecurlyWrapper +const querystring = require('querystring') +const crypto = require('crypto') +const request = require('request') +const Settings = require('settings-sharelatex') +const xml2js = require('xml2js') +const logger = require('logger-sharelatex') +const Async = require('async') + +module.exports = RecurlyWrapper = { + apiUrl: + __guard__( + Settings.apis != null ? Settings.apis.recurly : undefined, + x => x.url + ) || 'https://api.recurly.com/v2', + + _addressToXml(address) { + const allowedKeys = [ + 'address1', + 'address2', + 'city', + 'country', + 'state', + 'zip', + 'postal_code' + ] + let resultString = '\n' + for (let k in address) { + const v = address[k] + if (k === 'postal_code') { + k = 'zip' + } + if (v && Array.from(allowedKeys).includes(k)) { + resultString += `<${k}${k === 'address2' ? ' nil="nil"' : ''}>${v || + ''}\n` + } + } + resultString += '\n' + return resultString + }, + + _paypal: { + checkAccountExists(cache, next) { + const { user } = cache + const { recurly_token_id } = cache + const { subscriptionDetails } = cache + logger.log( + { user_id: user._id, recurly_token_id }, + 'checking if recurly account exists for user' + ) + return RecurlyWrapper.apiRequest( + { + url: `accounts/${user._id}`, + method: 'GET', + expect404: true + }, + function(error, response, responseBody) { + if (error) { + logger.error( + { error, user_id: user._id, recurly_token_id }, + 'error response from recurly while checking account' + ) + return next(error) + } + if (response.statusCode === 404) { + // actually not an error in this case, just no existing account + logger.log( + { user_id: user._id, recurly_token_id }, + 'user does not currently exist in recurly, proceed' + ) + cache.userExists = false + return next(null, cache) + } + logger.log( + { user_id: user._id, recurly_token_id }, + 'user appears to exist in recurly' + ) + return RecurlyWrapper._parseAccountXml(responseBody, function( + 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) { + const { user } = cache + const { recurly_token_id } = cache + const { subscriptionDetails } = cache + const { address } = subscriptionDetails + 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' + ) + const requestBody = `\ + + ${user._id} + ${user.email} + ${user.first_name} + ${user.last_name} +
+ ${address.address1} + ${address.address2} + ${address.city || ''} + ${address.state || ''} + ${address.zip || ''} + ${address.country} +
+
\ +` + return 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) + } + return RecurlyWrapper._parseAccountXml(responseBody, function( + 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) { + const { user } = cache + const { recurly_token_id } = cache + const { subscriptionDetails } = cache + logger.log( + { user_id: user._id, recurly_token_id }, + 'creating billing info in recurly' + ) + const accountCode = __guard__( + cache != null ? cache.account : undefined, + x1 => x1.account_code + ) + if (!accountCode) { + return next(new Error('no account code at createBillingInfo stage')) + } + const requestBody = `\ + + ${recurly_token_id} +\ +` + return 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) + } + return RecurlyWrapper._parseBillingInfoXml(responseBody, function( + 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) { + const { user } = cache + const { recurly_token_id } = cache + const { subscriptionDetails } = cache + logger.log( + { user_id: user._id, recurly_token_id }, + 'setting billing address in recurly' + ) + const accountCode = __guard__( + cache != null ? cache.account : undefined, + x1 => x1.account_code + ) + if (!accountCode) { + return next(new Error('no account code at setAddress stage')) + } + const { address } = subscriptionDetails + if (!address) { + return next( + new Error('no address in subscriptionDetails at setAddress stage') + ) + } + const requestBody = RecurlyWrapper._addressToXml(address) + return 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) + } + return RecurlyWrapper._parseBillingInfoXml(responseBody, function( + 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) { + const { user } = cache + const { recurly_token_id } = cache + const { subscriptionDetails } = cache + logger.log( + { user_id: user._id, recurly_token_id }, + 'creating subscription in recurly' + ) + const requestBody = `\ + + ${subscriptionDetails.plan_code} + ${subscriptionDetails.currencyCode} + ${subscriptionDetails.coupon_code} + + ${user._id} + +\ +` // TODO: check account details and billing + return 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) + } + return RecurlyWrapper._parseSubscriptionXml(responseBody, function( + 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 + const cache = { user, recurly_token_id, subscriptionDetails } + return Async.waterfall( + [ + Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache), + RecurlyWrapper._paypal.createAccount, + RecurlyWrapper._paypal.createBillingInfo, + RecurlyWrapper._paypal.setAddress, + RecurlyWrapper._paypal.createSubscription + ], + function(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' + ) + return callback(null, result.subscription) + } + ) + }, + + _createCreditCardSubscription( + user, + subscriptionDetails, + recurly_token_id, + callback + ) { + const requestBody = `\ + + ${subscriptionDetails.plan_code} + ${subscriptionDetails.currencyCode} + ${subscriptionDetails.coupon_code} + + ${user._id} + ${user.email} + ${user.first_name} + ${user.last_name} + + ${recurly_token_id} + + +\ +` + return RecurlyWrapper.apiRequest( + { + url: 'subscriptions', + method: 'POST', + body: requestBody + }, + (error, response, responseBody) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseSubscriptionXml(responseBody, callback) + } + ) + }, + + createSubscription(user, subscriptionDetails, recurly_token_id, callback) { + const { isPaypal } = subscriptionDetails + logger.log( + { user_id: user._id, isPaypal, recurly_token_id }, + 'setting up subscription in recurly' + ) + const fn = isPaypal + ? RecurlyWrapper._createPaypalSubscription + : RecurlyWrapper._createCreditCardSubscription + return fn(user, subscriptionDetails, recurly_token_id, callback) + }, + + apiRequest(options, callback) { + options.url = RecurlyWrapper.apiUrl + '/' + options.url + options.headers = { + Authorization: `Basic ${new Buffer(Settings.apis.recurly.apiKey).toString( + 'base64' + )}`, + Accept: 'application/xml', + 'Content-Type': 'application/xml; charset=utf-8' + } + const { expect404 } = options + delete options.expect404 + return request(options, function(error, response, body) { + if ( + error == null && + response.statusCode !== 200 && + response.statusCode !== 201 && + response.statusCode !== 204 && + (response.statusCode !== 404 || !expect404) + ) { + logger.err( + { + err: error, + body, + options, + statusCode: response != null ? response.statusCode : undefined + }, + 'error returned from recurly' + ) + error = `Recurly API returned with status code: ${response.statusCode}` + } + if (response.statusCode === 404 && expect404) { + logger.log( + { url: options.url, method: options.method }, + 'got 404 response from recurly, expected as valid response' + ) + } + return callback(error, response, body) + }) + }, + + getSubscriptions(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}/subscriptions` + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, callback) + } + ) + }, + + getSubscription(subscriptionId, options, callback) { + let url + if (callback == null) { + callback = options + } + if (!options) { + options = {} + } + + if (options.recurlyJsResult) { + url = `recurly_js/result/${subscriptionId}` + } else { + url = `subscriptions/${subscriptionId}` + } + + return RecurlyWrapper.apiRequest( + { + url + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseSubscriptionXml( + body, + (error, recurlySubscription) => { + if (error != null) { + return callback(error) + } + if (options.includeAccount) { + let accountId + if ( + recurlySubscription.account != null && + recurlySubscription.account.url != null + ) { + accountId = recurlySubscription.account.url.match( + /accounts\/(.*)/ + )[1] + } else { + return callback("I don't understand the response from Recurly") + } + + return RecurlyWrapper.getAccount(accountId, function( + error, + account + ) { + if (error != null) { + return callback(error) + } + recurlySubscription.account = account + return callback(null, recurlySubscription) + }) + } else { + return callback(null, recurlySubscription) + } + } + ) + } + ) + }, + + getAccounts(callback) { + let allAccounts = [] + var getPageOfAccounts = (cursor = null) => { + const opts = { + url: 'accounts', + qs: { + per_page: 200 + } + } + if (cursor != null) { + opts.qs.cursor = cursor + } + return RecurlyWrapper.apiRequest(opts, (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, function(err, data) { + if (err != null) { + logger.err({ err }, 'could not get accoutns') + callback(err) + } + allAccounts = allAccounts.concat(data.accounts) + logger.log( + `got another ${data.accounts.length}, total now ${ + allAccounts.length + }` + ) + cursor = __guard__( + response.headers.link != null + ? response.headers.link.match(/cursor=([0-9]+)&/) + : undefined, + x1 => x1[1] + ) + if (cursor != null) { + return getPageOfAccounts(cursor) + } else { + return callback(err, allAccounts) + } + }) + }) + } + + return getPageOfAccounts() + }, + + getAccount(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}` + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseAccountXml(body, callback) + } + ) + }, + + getAccountActiveCoupons(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}/redemptions` + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseRedemptionsXml(body, function( + error, + redemptions + ) { + if (error != null) { + return callback(error) + } + const activeRedemptions = redemptions.filter( + redemption => redemption.state === 'active' + ) + const couponCodes = activeRedemptions.map( + redemption => redemption.coupon_code + ) + return Async.map(couponCodes, RecurlyWrapper.getCoupon, function( + error, + coupons + ) { + if (error != null) { + return callback(error) + } + return callback(null, coupons) + }) + }) + } + ) + }, + + getCoupon(couponCode, callback) { + const opts = { url: `coupons/${couponCode}` } + return RecurlyWrapper.apiRequest(opts, (error, response, body) => + RecurlyWrapper._parseCouponXml(body, callback) + ) + }, + + getBillingInfo(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}/billing_info` + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, callback) + } + ) + }, + + updateSubscription(subscriptionId, options, callback) { + logger.log( + { subscriptionId, options }, + 'telling recurly to update subscription' + ) + const requestBody = `\ + + ${options.plan_code} + ${options.timeframe} +\ +` + return RecurlyWrapper.apiRequest( + { + url: `subscriptions/${subscriptionId}`, + method: 'put', + body: requestBody + }, + (error, response, responseBody) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseSubscriptionXml(responseBody, callback) + } + ) + }, + + createFixedAmmountCoupon( + coupon_code, + name, + currencyCode, + discount_in_cents, + plan_code, + callback + ) { + const requestBody = `\ + + ${coupon_code} + ${name} + dollars + + <${currencyCode}>${discount_in_cents} + + + ${plan_code} + + false +\ +` + logger.log({ coupon_code, requestBody }, 'creating coupon') + return RecurlyWrapper.apiRequest( + { + url: 'coupons', + method: 'post', + body: requestBody + }, + (error, response, responseBody) => { + if (error != null) { + logger.err({ err: error, coupon_code }, 'error creating coupon') + } + return callback(error) + } + ) + }, + + lookupCoupon(coupon_code, callback) { + return RecurlyWrapper.apiRequest( + { + url: `coupons/${coupon_code}` + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, callback) + } + ) + }, + + cancelSubscription(subscriptionId, callback) { + logger.log({ subscriptionId }, 'telling recurly to cancel subscription') + return RecurlyWrapper.apiRequest( + { + url: `subscriptions/${subscriptionId}/cancel`, + method: 'put' + }, + function(error, response, body) { + if (error != null) { + return RecurlyWrapper._parseXml(body, function(_err, parsed) { + if ( + __guard__( + parsed != null ? parsed.error : undefined, + x1 => x1.description + ) === "A canceled subscription can't transition to canceled" + ) { + logger.log( + { subscriptionId, error, body }, + 'subscription already cancelled, not really an error, proceeding' + ) + return callback(null) + } else { + return callback(error) + } + }) + } else { + return callback(null) + } + } + ) + }, + + reactivateSubscription(subscriptionId, callback) { + logger.log( + { subscriptionId }, + 'telling recurly to reactivating subscription' + ) + return RecurlyWrapper.apiRequest( + { + url: `subscriptions/${subscriptionId}/reactivate`, + method: 'put' + }, + (error, response, body) => callback(error) + ) + }, + + redeemCoupon(account_code, coupon_code, callback) { + const requestBody = `\ + + ${account_code} + USD +\ +` + logger.log( + { account_code, coupon_code, requestBody }, + 'redeeming coupon for user' + ) + return RecurlyWrapper.apiRequest( + { + url: `coupons/${coupon_code}/redeem`, + method: 'post', + body: requestBody + }, + (error, response, responseBody) => { + if (error != null) { + logger.err( + { err: error, account_code, coupon_code }, + 'error redeeming coupon' + ) + } + return callback(error) + } + ) + }, + + extendTrial(subscriptionId, daysUntilExpire, callback) { + if (daysUntilExpire == null) { + daysUntilExpire = 7 + } + const next_renewal_date = new Date() + next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire) + logger.log( + { subscriptionId, daysUntilExpire }, + 'Exending Free trial for user' + ) + return RecurlyWrapper.apiRequest( + { + url: `/subscriptions/${subscriptionId}/postpone?next_renewal_date=${next_renewal_date}&bulk=false`, + method: 'put' + }, + (error, response, responseBody) => { + if (error != null) { + logger.err( + { err: error, subscriptionId, daysUntilExpire }, + 'error exending trial' + ) + } + return callback(error) + } + ) + }, + + listAccountActiveSubscriptions(account_id, callback) { + if (callback == null) { + callback = function(error, subscriptions) {} + } + return RecurlyWrapper.apiRequest( + { + url: `accounts/${account_id}/subscriptions`, + qs: { + state: 'active' + }, + expect404: true + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + if (response.statusCode === 404) { + return callback(null, []) + } else { + return RecurlyWrapper._parseSubscriptionsXml(body, callback) + } + } + ) + }, + + _parseSubscriptionsXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute( + xml, + 'subscriptions', + callback + ) + }, + + _parseSubscriptionXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute( + xml, + 'subscription', + callback + ) + }, + + _parseAccountXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'account', callback) + }, + + _parseBillingInfoXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute( + xml, + 'billing_info', + callback + ) + }, + + _parseRedemptionsXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'redemptions', callback) + }, + + _parseCouponXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'coupon', callback) + }, + + _parseXmlAndGetAttribute(xml, attribute, callback) { + return RecurlyWrapper._parseXml(xml, function(error, data) { + if (error != null) { + return callback(error) + } + if (data != null && data[attribute] != null) { + return callback(null, data[attribute]) + } else { + return callback( + new Error("I don't understand the response from Recurly") + ) + } + }) + }, + + _parseXml(xml, callback) { + var convertDataTypes = function(data) { + let key, value + if (data != null && data['$'] != null) { + if (data['$']['nil'] === 'nil') { + data = null + } else if (data['$'].href != null) { + data.url = data['$'].href + delete data['$'] + } else if (data['$']['type'] === 'integer') { + data = parseInt(data['_'], 10) + } else if (data['$']['type'] === 'datetime') { + data = new Date(data['_']) + } else if (data['$']['type'] === 'array') { + delete data['$'] + let array = [] + for (key in data) { + value = data[key] + if (value instanceof Array) { + array = array.concat(convertDataTypes(value)) + } else { + array.push(convertDataTypes(value)) + } + } + data = array + } + } + + if (data instanceof Array) { + data = Array.from(data).map(entry => convertDataTypes(entry)) + } else if (typeof data === 'object') { + for (key in data) { + value = data[key] + data[key] = convertDataTypes(value) + } + } + return data + } + + const parser = new xml2js.Parser({ + explicitRoot: true, + explicitArray: false, + emptyTag: '' + }) + return parser.parseString(xml, function(error, data) { + if (error != null) { + return callback(error) + } + const result = convertDataTypes(data) + return callback(null, result) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js new file mode 100644 index 0000000000..9ddc31b327 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -0,0 +1,474 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SubscriptionController +const AuthenticationController = require('../Authentication/AuthenticationController') +const SubscriptionHandler = require('./SubscriptionHandler') +const PlansLocator = require('./PlansLocator') +const SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder') +const LimitationsManager = require('./LimitationsManager') +const RecurlyWrapper = require('./RecurlyWrapper') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const GeoIpLookup = require('../../infrastructure/GeoIpLookup') +const UserGetter = require('../User/UserGetter') +const FeaturesUpdater = require('./FeaturesUpdater') +const planFeatures = require('./planFeatures') +const GroupPlansData = require('./GroupPlansData') +const V1SubscriptionManager = require('./V1SubscriptionManager') + +module.exports = SubscriptionController = { + plansPage(req, res, next) { + const plans = SubscriptionViewModelBuilder.buildViewModel() + let viewName = 'subscriptions/plans' + if (req.query.v != null) { + viewName = `${viewName}_${req.query.v}` + } + logger.log({ viewName }, 'showing plans page') + let currentUser = null + + return GeoIpLookup.getCurrencyCode( + (req.query != null ? req.query.ip : undefined) || req.ip, + function(err, recomendedCurrency) { + if (err != null) { + return next(err) + } + const render = () => + res.render(viewName, { + title: 'plans_and_pricing', + plans, + gaExperiments: Settings.gaExperiments.plansPage, + recomendedCurrency, + planFeatures, + groupPlans: GroupPlansData + }) + const user_id = AuthenticationController.getLoggedInUserId(req) + if (user_id != null) { + return UserGetter.getUser(user_id, { signUpDate: 1 }, function( + err, + user + ) { + if (err != null) { + return next(err) + } + currentUser = user + return render() + }) + } else { + return render() + } + } + ) + }, + + // get to show the recurly.js page + paymentPage(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) + return LimitationsManager.userHasV1OrV2Subscription(user, function( + err, + hasSubscription + ) { + if (err != null) { + return next(err) + } + if (hasSubscription || plan == null) { + return res.redirect('/user/subscription?hasSubscription=true') + } else { + // LimitationsManager.userHasV2Subscription only checks Mongo. Double check with + // Recurly as well at this point (we don't do this most places for speed). + return SubscriptionHandler.validateNoSubscriptionInRecurly( + user._id, + function(error, valid) { + if (error != null) { + return next(error) + } + if (!valid) { + res.redirect('/user/subscription?hasSubscription=true') + } else { + let currency = + req.query.currency != null + ? req.query.currency.toUpperCase() + : undefined + return GeoIpLookup.getCurrencyCode( + (req.query != null ? req.query.ip : undefined) || req.ip, + function(err, recomendedCurrency, countryCode) { + if (err != null) { + return next(err) + } + if (recomendedCurrency != null && currency == null) { + currency = recomendedCurrency + } + return res.render('subscriptions/new', { + title: 'subscribe', + plan_code: req.query.planCode, + currency, + countryCode, + plan, + showStudentPlan: req.query.ssp, + recurlyConfig: JSON.stringify({ + currency, + subdomain: Settings.apis.recurly.subdomain + }), + showCouponField: req.query.scf, + showVatField: req.query.svf, + couponCode: req.query.cc || '' + }) + } + ) + } + } + ) + } + }) + }, + + userSubscriptionPage(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + user, + function(error, results) { + if (error != null) { + return next(error) + } + const { + personalSubscription, + memberGroupSubscriptions, + managedGroupSubscriptions, + confirmedMemberInstitutions, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus + } = results + return LimitationsManager.userHasV1OrV2Subscription(user, function( + err, + hasSubscription + ) { + if (error != null) { + return next(error) + } + const fromPlansPage = req.query.hasSubscription + logger.log( + { + user, + hasSubscription, + fromPlansPage, + personalSubscription, + memberGroupSubscriptions, + managedGroupSubscriptions, + confirmedMemberInstitutions, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus + }, + 'showing subscription dashboard' + ) + const plans = SubscriptionViewModelBuilder.buildViewModel() + const data = { + title: 'your_subscription', + plans, + user, + hasSubscription, + fromPlansPage, + personalSubscription, + memberGroupSubscriptions, + managedGroupSubscriptions, + confirmedMemberInstitutions, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus + } + return res.render('subscriptions/dashboard', data) + }) + } + ) + }, + + createSubscription(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + const { recurly_token_id } = req.body + const { subscriptionDetails } = req.body + logger.log( + { recurly_token_id, user_id: user._id, subscriptionDetails }, + 'creating subscription' + ) + + return LimitationsManager.userHasV1OrV2Subscription(user, function( + err, + hasSubscription + ) { + if (err != null) { + return next(err) + } + if (hasSubscription) { + logger.warn({ user_id: user._id }, 'user already has subscription') + res.sendStatus(409) // conflict + } + return SubscriptionHandler.createSubscription( + user, + subscriptionDetails, + recurly_token_id, + function(err) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'something went wrong creating subscription' + ) + return next(err) + } + return res.sendStatus(201) + } + ) + }) + }, + + successful_subscription(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + user, + function(error, { personalSubscription }) { + if (error != null) { + return next(error) + } + if (personalSubscription == null) { + return res.redirect('/user/subscription/plans') + } + return res.render('subscriptions/successful_subscription', { + title: 'thank_you', + personalSubscription + }) + } + ) + }, + + cancelSubscription(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + logger.log({ user_id: user._id }, 'canceling subscription') + return SubscriptionHandler.cancelSubscription(user, function(err) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'something went wrong canceling subscription' + ) + return next(err) + } + // Note: this redirect isn't used in the main flow as the redirection is + // handled by Angular + return res.redirect('/user/subscription/canceled') + }) + }, + + canceledSubscription(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + return res.render('subscriptions/canceled_subscription', { + title: 'subscription_canceled' + }) + }, + + cancelV1Subscription(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ user_id }, 'canceling v1 subscription') + return V1SubscriptionManager.cancelV1Subscription(user_id, function(err) { + if (err != null) { + logger.err( + { err, user_id }, + 'something went wrong canceling v1 subscription' + ) + return next(err) + } + return res.redirect('/user/subscription') + }) + }, + + updateSubscription(req, res, next) { + const _origin = + __guard__(req != null ? req.query : undefined, x => x.origin) || null + const user = AuthenticationController.getSessionUser(req) + const planCode = req.body.plan_code + if (planCode == null) { + const err = new Error('plan_code is not defined') + logger.err( + { user_id: user._id, err, planCode, origin: _origin, body: req.body }, + '[Subscription] error in updateSubscription form' + ) + return next(err) + } + logger.log({ planCode, user_id: user._id }, 'updating subscription') + return SubscriptionHandler.updateSubscription( + user, + planCode, + null, + function(err) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'something went wrong updating subscription' + ) + return next(err) + } + return res.redirect('/user/subscription') + } + ) + }, + + reactivateSubscription(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + logger.log({ user_id: user._id }, 'reactivating subscription') + return SubscriptionHandler.reactivateSubscription(user, function(err) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'something went wrong reactivating subscription' + ) + return next(err) + } + return res.redirect('/user/subscription') + }) + }, + + recurlyCallback(req, res, next) { + logger.log({ data: req.body }, 'received recurly callback') + // we only care if a subscription has exipired + if ( + req.body != null && + req.body['expired_subscription_notification'] != null + ) { + const recurlySubscription = + req.body['expired_subscription_notification'].subscription + return SubscriptionHandler.recurlyCallback(recurlySubscription, function( + err + ) { + if (err != null) { + return next(err) + } + return res.sendStatus(200) + }) + } else { + return res.sendStatus(200) + } + }, + + renderUpgradeToAnnualPlanPage(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + return LimitationsManager.userHasV2Subscription(user, function( + err, + hasSubscription, + subscription + ) { + let planName + if (err != null) { + return next(err) + } + const planCode = + subscription != null ? subscription.planCode.toLowerCase() : undefined + if ((planCode != null ? planCode.indexOf('annual') : undefined) !== -1) { + planName = 'annual' + } else if ( + (planCode != null ? planCode.indexOf('student') : undefined) !== -1 + ) { + planName = 'student' + } else if ( + (planCode != null ? planCode.indexOf('collaborator') : undefined) !== -1 + ) { + planName = 'collaborator' + } + if (!hasSubscription) { + return res.redirect('/user/subscription/plans') + } + logger.log( + { planName, user_id: user._id }, + 'rendering upgrade to annual page' + ) + return res.render('subscriptions/upgradeToAnnual', { + title: 'Upgrade to annual', + planName + }) + }) + }, + + processUpgradeToAnnualPlan(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + const { planName } = req.body + const coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName] + const annualPlanName = `${planName}-annual` + logger.log( + { user_id: user._id, planName: annualPlanName }, + 'user is upgrading to annual billing with discount' + ) + return SubscriptionHandler.updateSubscription( + user, + annualPlanName, + coupon_code, + function(err) { + if (err != null) { + logger.err({ err, user_id: user._id }, 'error updating subscription') + return next(err) + } + return res.sendStatus(200) + } + ) + }, + + extendTrial(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + return LimitationsManager.userHasV2Subscription(user, function( + err, + hasSubscription, + subscription + ) { + if (err != null) { + return next(err) + } + return SubscriptionHandler.extendTrial(subscription, 14, function(err) { + if (err != null) { + return res.send(500) + } else { + return res.send(200) + } + }) + }) + }, + + recurlyNotificationParser(req, res, next) { + let xml = '' + req.on('data', chunk => (xml += chunk)) + return req.on('end', () => + RecurlyWrapper._parseXml(xml, function(error, body) { + if (error != null) { + return next(error) + } + req.body = body + return next() + }) + ) + }, + + refreshUserFeatures(req, res, next) { + const { user_id } = req.params + return FeaturesUpdater.refreshFeatures(user_id, function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionFormatters.js b/services/web/app/src/Features/Subscription/SubscriptionFormatters.js new file mode 100644 index 0000000000..b47d6f84a4 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionFormatters.js @@ -0,0 +1,56 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const dateformat = require('dateformat') +const settings = require('settings-sharelatex') + +const currenySymbols = { + EUR: '€', + USD: '$', + GBP: '£', + SEK: 'kr', + CAD: '$', + NOK: 'kr', + DKK: 'kr', + AUD: '$', + NZD: '$', + CHF: 'Fr', + SGD: '$' +} + +module.exports = { + formatPrice(priceInCents, currency) { + if (currency == null) { + currency = 'USD' + } + let string = priceInCents + '' + if (string.length === 2) { + string = `0${string}` + } + if (string.length === 1) { + string = `00${string}` + } + if (string.length === 0) { + string = '000' + } + const cents = string.slice(-2) + const dollars = string.slice(0, -2) + const symbol = currenySymbols[currency] + return `${symbol}${dollars}.${cents}` + }, + + formatDate(date) { + if (date == null) { + return null + } + return dateformat(date, 'dS mmmm yyyy') + } +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.js b/services/web/app/src/Features/Subscription/SubscriptionGroupController.js new file mode 100644 index 0000000000..13fcf8797e --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.js @@ -0,0 +1,102 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SubscriptionGroupHandler = require('./SubscriptionGroupHandler') +const logger = require('logger-sharelatex') +const SubscriptionLocator = require('./SubscriptionLocator') +const AuthenticationController = require('../Authentication/AuthenticationController') +const _ = require('underscore') +const async = require('async') + +module.exports = { + removeUserFromGroup(req, res, next) { + const subscription = req.entity + const userToRemove_id = req.params.user_id + logger.log( + { subscriptionId: subscription._id, userToRemove_id }, + 'removing user from group subscription' + ) + return SubscriptionGroupHandler.removeUserFromGroup( + subscription._id, + userToRemove_id, + function(err) { + if (err != null) { + logger.err( + { err, subscriptionId: subscription._id, userToRemove_id }, + 'error removing user from group' + ) + return next(err) + } + return res.send() + } + ) + }, + + removeSelfFromGroup(req, res, next) { + const adminUserId = req.query.admin_user_id + const userToRemove_id = AuthenticationController.getLoggedInUserId(req) + return getManagedSubscription(adminUserId, function(error, subscription) { + if (error != null) { + return next(error) + } + logger.log( + { adminUserId, userToRemove_id }, + 'removing user from group subscription after self request' + ) + return SubscriptionGroupHandler.removeUserFromGroup( + subscription._id, + userToRemove_id, + function(err) { + if (err != null) { + logger.err( + { err, userToRemove_id, adminUserId }, + 'error removing self from group' + ) + return res.sendStatus(500) + } + return res.send() + } + ) + }) + }, + + // legacy route + redirectToSubscriptionGroupAdminPage(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return getManagedSubscription(user_id, function(error, subscription) { + if (error != null) { + return next(error) + } + if (!(subscription != null ? subscription.groupPlan : undefined)) { + return res.redirect('/user/subscription') + } + return res.redirect(`/manage/groups/${subscription._id}/members`) + }) + } +} + +var getManagedSubscription = (managerId, callback) => + SubscriptionLocator.findManagedSubscription(managerId, function( + err, + subscription + ) { + if (subscription != null) { + logger.log({ managerId }, 'got managed subscription') + } else { + if (!err) { + err = new Error(`No subscription found managed by user ${managerId}`) + } + } + + return callback(err, subscription) + }) diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js new file mode 100644 index 0000000000..d086315497 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js @@ -0,0 +1,155 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SubscriptionGroupHandler +const async = require('async') +const _ = require('underscore') +const SubscriptionUpdater = require('./SubscriptionUpdater') +const SubscriptionLocator = require('./SubscriptionLocator') +const UserGetter = require('../User/UserGetter') +const { Subscription } = require('../../models/Subscription') +const LimitationsManager = require('./LimitationsManager') +const logger = require('logger-sharelatex') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const EmailHandler = require('../Email/EmailHandler') +const settings = require('settings-sharelatex') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const UserMembershipViewModel = require('../UserMembership/UserMembershipViewModel') + +module.exports = SubscriptionGroupHandler = { + removeUserFromGroup(subscriptionId, userToRemove_id, callback) { + return SubscriptionUpdater.removeUserFromGroup( + subscriptionId, + userToRemove_id, + callback + ) + }, + + replaceUserReferencesInGroups(oldId, newId, callback) { + logger.log( + { old_id: oldId, new_id: newId }, + 'replacing user reference in groups' + ) + return Subscription.update( + { admin_id: oldId }, + { admin_id: newId }, + function(error) { + if (error != null) { + return callback(error) + } + + return replaceInArray( + Subscription, + 'manager_ids', + oldId, + newId, + function(error) { + if (error != null) { + return callback(error) + } + + return replaceInArray( + Subscription, + 'member_ids', + oldId, + newId, + callback + ) + } + ) + } + ) + }, + + isUserPartOfGroup(user_id, subscription_id, callback) { + if (callback == null) { + callback = function(err, partOfGroup) {} + } + return SubscriptionLocator.getSubscriptionByMemberIdAndId( + user_id, + subscription_id, + function(err, subscription) { + let partOfGroup + if (subscription != null) { + partOfGroup = true + } else { + partOfGroup = false + } + logger.log( + { user_id, subscription_id, partOfGroup }, + 'checking if user is part of a group' + ) + return callback(err, partOfGroup) + } + ) + }, + + getTotalConfirmedUsersInGroup(subscription_id, callback) { + if (callback == null) { + callback = function(err, totalUsers) {} + } + return SubscriptionLocator.getSubscription( + subscription_id, + (err, subscription) => + callback( + err, + __guard__( + subscription != null ? subscription.member_ids : undefined, + x => x.length + ) + ) + ) + } +} + +var replaceInArray = function(model, property, oldValue, newValue, callback) { + logger.log( + `Replacing ${oldValue} with ${newValue} in ${property} of ${model}` + ) + + // Mongo won't let us pull and addToSet in the same query, so do it in + // two. Note we need to add first, since the query is based on the old user. + const query = {} + query[property] = oldValue + + const setNewValue = {} + setNewValue[property] = newValue + + const setOldValue = {} + setOldValue[property] = oldValue + + return model.update( + query, + { $addToSet: setNewValue }, + { multi: true }, + function(error) { + if (error != null) { + return callback(error) + } + return model.update( + query, + { $pull: setOldValue }, + { multi: true }, + callback + ) + } + ) +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionHandler.js b/services/web/app/src/Features/Subscription/SubscriptionHandler.js new file mode 100644 index 0000000000..10436e1635 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionHandler.js @@ -0,0 +1,247 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async') +const RecurlyWrapper = require('./RecurlyWrapper') +const Settings = require('settings-sharelatex') +const { User } = require('../../models/User') +const logger = require('logger-sharelatex') +const SubscriptionUpdater = require('./SubscriptionUpdater') +const LimitationsManager = require('./LimitationsManager') +const EmailHandler = require('../Email/EmailHandler') +const Events = require('../../infrastructure/Events') +const Analytics = require('../Analytics/AnalyticsManager') + +module.exports = { + validateNoSubscriptionInRecurly(user_id, callback) { + if (callback == null) { + callback = function(error, valid) {} + } + return RecurlyWrapper.listAccountActiveSubscriptions(user_id, function( + error, + subscriptions + ) { + if (subscriptions == null) { + subscriptions = [] + } + if (error != null) { + return callback(error) + } + if (subscriptions.length > 0) { + return SubscriptionUpdater.syncSubscription( + subscriptions[0], + user_id, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, false) + } + ) + } else { + return callback(null, true) + } + }) + }, + + createSubscription(user, subscriptionDetails, recurly_token_id, callback) { + const self = this + const clientTokenId = '' + return this.validateNoSubscriptionInRecurly(user._id, function( + error, + valid + ) { + if (error != null) { + return callback(error) + } + if (!valid) { + return callback(new Error('user already has subscription in recurly')) + } + return RecurlyWrapper.createSubscription( + user, + subscriptionDetails, + recurly_token_id, + function(error, recurlySubscription) { + if (error != null) { + return callback(error) + } + return SubscriptionUpdater.syncSubscription( + recurlySubscription, + user._id, + function(error) { + if (error != null) { + return callback(error) + } + return callback() + } + ) + } + ) + }) + }, + + updateSubscription(user, plan_code, coupon_code, callback) { + logger.log({ user, plan_code, coupon_code }, 'updating subscription') + return LimitationsManager.userHasV2Subscription(user, function( + err, + hasSubscription, + subscription + ) { + if (!hasSubscription) { + return callback() + } else { + return async.series( + [ + function(cb) { + if (coupon_code == null) { + return cb() + } + logger.log( + { user_id: user._id, plan_code, coupon_code }, + 'updating subscription with coupon code applied first' + ) + return RecurlyWrapper.getSubscription( + subscription.recurlySubscription_id, + { includeAccount: true }, + function(err, usersSubscription) { + if (err != null) { + return callback(err) + } + const { account_code } = usersSubscription.account + return RecurlyWrapper.redeemCoupon( + account_code, + coupon_code, + cb + ) + } + ) + }, + cb => + RecurlyWrapper.updateSubscription( + subscription.recurlySubscription_id, + { plan_code, timeframe: 'now' }, + function(error, recurlySubscription) { + if (error != null) { + return callback(error) + } + return SubscriptionUpdater.syncSubscription( + recurlySubscription, + user._id, + cb + ) + } + ) + ], + callback + ) + } + }) + }, + + cancelSubscription(user, callback) { + return LimitationsManager.userHasV2Subscription(user, function( + err, + hasSubscription, + subscription + ) { + if (hasSubscription) { + return RecurlyWrapper.cancelSubscription( + subscription.recurlySubscription_id, + function(error) { + if (error != null) { + return callback(error) + } + const emailOpts = { + to: user.email, + first_name: user.first_name + } + const ONE_HOUR_IN_MS = 1000 * 60 * 60 + setTimeout( + () => EmailHandler.sendEmail('canceledSubscription', emailOpts), + ONE_HOUR_IN_MS + ) + Events.emit('cancelSubscription', user._id) + Analytics.recordEvent(user._id, 'subscription-canceled') + return callback() + } + ) + } else { + return callback() + } + }) + }, + + reactivateSubscription(user, callback) { + return LimitationsManager.userHasV2Subscription(user, function( + err, + hasSubscription, + subscription + ) { + if (hasSubscription) { + return RecurlyWrapper.reactivateSubscription( + subscription.recurlySubscription_id, + function(error) { + if (error != null) { + return callback(error) + } + EmailHandler.sendEmail('reactivatedSubscription', { + to: user.email + }) + Analytics.recordEvent(user._id, 'subscription-reactivated') + return callback() + } + ) + } else { + return callback() + } + }) + }, + + recurlyCallback(recurlySubscription, callback) { + return RecurlyWrapper.getSubscription( + recurlySubscription.uuid, + { includeAccount: true }, + function(error, recurlySubscription) { + if (error != null) { + return callback(error) + } + return User.findById(recurlySubscription.account.account_code, function( + error, + user + ) { + if (error != null) { + return callback(error) + } + if (user == null) { + return callback('no user found') + } + return SubscriptionUpdater.syncSubscription( + recurlySubscription, + user != null ? user._id : undefined, + callback + ) + }) + } + ) + }, + + extendTrial(subscription, daysToExend, callback) { + return RecurlyWrapper.extendTrial( + subscription.recurlySubscription_id, + daysToExend, + callback + ) + } +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionLocator.js b/services/web/app/src/Features/Subscription/SubscriptionLocator.js new file mode 100644 index 0000000000..2dd25512d4 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionLocator.js @@ -0,0 +1,94 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SubscriptionLocator +const { Subscription } = require('../../models/Subscription') +const logger = require('logger-sharelatex') +const { ObjectId } = require('mongoose').Types + +module.exports = SubscriptionLocator = { + getUsersSubscription(user_or_id, callback) { + const user_id = this._getUserId(user_or_id) + logger.log({ user_id }, 'getting users subscription') + return Subscription.findOne({ admin_id: user_id }, function( + err, + subscription + ) { + logger.log({ user_id }, 'got users subscription') + return callback(err, subscription) + }) + }, + + findManagedSubscription(managerId, callback) { + logger.log({ managerId }, 'finding managed subscription') + return Subscription.findOne({ manager_ids: managerId }, callback) + }, + + getManagedGroupSubscriptions(user_or_id, callback) { + if (callback == null) { + callback = function(error, managedSubscriptions) {} + } + const user_id = this._getUserId(user_or_id) + return Subscription.find({ + manager_ids: user_or_id, + groupPlan: true + }) + .populate('admin_id') + .exec(callback) + }, + + getMemberSubscriptions(user_or_id, callback) { + const user_id = this._getUserId(user_or_id) + logger.log({ user_id }, 'getting users group subscriptions') + return Subscription.find({ member_ids: user_id }) + .populate('admin_id') + .exec(callback) + }, + + getSubscription(subscription_id, callback) { + return Subscription.findOne({ _id: subscription_id }, callback) + }, + + getSubscriptionByMemberIdAndId(user_id, subscription_id, callback) { + return Subscription.findOne( + { member_ids: user_id, _id: subscription_id }, + { _id: 1 }, + callback + ) + }, + + getGroupSubscriptionsMemberOf(user_id, callback) { + return Subscription.find( + { member_ids: user_id }, + { _id: 1, planCode: 1 }, + callback + ) + }, + + getGroupsWithEmailInvite(email, callback) { + return Subscription.find({ invited_emails: email }, callback) + }, + + getGroupWithV1Id(v1TeamId, callback) { + return Subscription.findOne({ 'overleaf.id': v1TeamId }, callback) + }, + + _getUserId(user_or_id) { + if (user_or_id != null && user_or_id._id != null) { + return user_or_id._id + } else if (user_or_id != null) { + return user_or_id + } + } +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionRouter.js b/services/web/app/src/Features/Subscription/SubscriptionRouter.js new file mode 100644 index 0000000000..7ccf8b3d07 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionRouter.js @@ -0,0 +1,137 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthenticationController = require('../Authentication/AuthenticationController') +const SubscriptionController = require('./SubscriptionController') +const SubscriptionGroupController = require('./SubscriptionGroupController') +const TeamInvitesController = require('./TeamInvitesController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const Settings = require('settings-sharelatex') + +module.exports = { + apply(webRouter, privateApiRouter, publicApiRouter) { + if (!Settings.enableSubscriptions) { + return + } + + webRouter.get('/user/subscription/plans', SubscriptionController.plansPage) + + webRouter.get( + '/user/subscription', + AuthenticationController.requireLogin(), + SubscriptionController.userSubscriptionPage + ) + + webRouter.get( + '/user/subscription/new', + AuthenticationController.requireLogin(), + SubscriptionController.paymentPage + ) + + webRouter.get( + '/user/subscription/thank-you', + AuthenticationController.requireLogin(), + SubscriptionController.successful_subscription + ) + + webRouter.get( + '/user/subscription/canceled', + AuthenticationController.requireLogin(), + SubscriptionController.canceledSubscription + ) + + webRouter.get( + '/subscription/group', + AuthenticationController.requireLogin(), + SubscriptionGroupController.redirectToSubscriptionGroupAdminPage + ) + webRouter.delete( + '/subscription/group/user', + AuthenticationController.requireLogin(), + SubscriptionGroupController.removeSelfFromGroup + ) + + // Team invites + webRouter.get( + '/subscription/invites/:token/', + AuthenticationController.requireLogin(), + TeamInvitesController.viewInvite + ) + webRouter.put( + '/subscription/invites/:token/', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'team-invite', + maxRequests: 10, + timeInterval: 60 + }), + TeamInvitesController.acceptInvite + ) + + // recurly callback + publicApiRouter.post( + '/user/subscription/callback', + SubscriptionController.recurlyNotificationParser, + SubscriptionController.recurlyCallback + ) + + // user changes their account state + webRouter.post( + '/user/subscription/create', + AuthenticationController.requireLogin(), + SubscriptionController.createSubscription + ) + webRouter.post( + '/user/subscription/update', + AuthenticationController.requireLogin(), + SubscriptionController.updateSubscription + ) + webRouter.post( + '/user/subscription/cancel', + AuthenticationController.requireLogin(), + SubscriptionController.cancelSubscription + ) + webRouter.post( + '/user/subscription/reactivate', + AuthenticationController.requireLogin(), + SubscriptionController.reactivateSubscription + ) + + webRouter.post( + '/user/subscription/v1/cancel', + AuthenticationController.requireLogin(), + SubscriptionController.cancelV1Subscription + ) + + webRouter.put( + '/user/subscription/extend', + AuthenticationController.requireLogin(), + SubscriptionController.extendTrial + ) + + webRouter.get( + '/user/subscription/upgrade-annual', + AuthenticationController.requireLogin(), + SubscriptionController.renderUpgradeToAnnualPlanPage + ) + webRouter.post( + '/user/subscription/upgrade-annual', + AuthenticationController.requireLogin(), + SubscriptionController.processUpgradeToAnnualPlan + ) + + // Currently used in acceptance tests only, as a way to trigger the syncing logic + return publicApiRouter.post( + '/user/:user_id/features/sync', + AuthenticationController.httpAuth, + SubscriptionController.refreshUserFeatures + ) + } +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js new file mode 100644 index 0000000000..23d991a5a1 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js @@ -0,0 +1,237 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SubscriptionUpdater +const async = require('async') +const _ = require('underscore') +const { Subscription } = require('../../models/Subscription') +const SubscriptionLocator = require('./SubscriptionLocator') +const UserGetter = require('../User/UserGetter') +const PlansLocator = require('./PlansLocator') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const { ObjectId } = require('mongoose').Types +const FeaturesUpdater = require('./FeaturesUpdater') + +const oneMonthInSeconds = 60 * 60 * 24 * 30 + +module.exports = SubscriptionUpdater = { + syncSubscription(recurlySubscription, adminUser_id, callback) { + logger.log( + { adminUser_id, recurlySubscription }, + 'syncSubscription, creating new if subscription does not exist' + ) + return SubscriptionLocator.getUsersSubscription(adminUser_id, function( + err, + subscription + ) { + if (err != null) { + return callback(err) + } + if (subscription != null) { + logger.log( + { adminUser_id, recurlySubscription }, + 'subscription does exist' + ) + return SubscriptionUpdater._updateSubscriptionFromRecurly( + recurlySubscription, + subscription, + callback + ) + } else { + logger.log( + { adminUser_id, recurlySubscription }, + 'subscription does not exist, creating a new one' + ) + return SubscriptionUpdater._createNewSubscription( + adminUser_id, + function(err, subscription) { + if (err != null) { + return callback(err) + } + return SubscriptionUpdater._updateSubscriptionFromRecurly( + recurlySubscription, + subscription, + callback + ) + } + ) + } + }) + }, + + addUserToGroup(subscriptionId, userId, callback) { + return this.addUsersToGroup(subscriptionId, [userId], callback) + }, + + addUsersToGroup(subscriptionId, memberIds, callback) { + return this.addUsersToGroupWithoutFeaturesRefresh( + subscriptionId, + memberIds, + function(err) { + if (err != null) { + return callback(err) + } + + // Only apply features updates to users, not user stubs + return UserGetter.getUsers(memberIds, { _id: 1 }, function(err, users) { + if (err != null) { + return callback(err) + } + + const userIds = users.map(u => u._id.toString()) + return async.map(userIds, FeaturesUpdater.refreshFeatures, callback) + }) + } + ) + }, + + addUsersToGroupWithoutFeaturesRefresh(subscriptionId, memberIds, callback) { + logger.log( + { subscriptionId, memberIds }, + 'adding members into mongo subscription' + ) + const searchOps = { _id: subscriptionId } + const insertOperation = { $addToSet: { member_ids: { $each: memberIds } } } + + return Subscription.findAndModify(searchOps, insertOperation, callback) + }, + + removeUserFromGroups(filter, user_id, callback) { + const removeOperation = { $pull: { member_ids: user_id } } + return Subscription.updateMany(filter, removeOperation, function(err) { + if (err != null) { + logger.err( + { err, searchOps, removeOperation }, + 'error removing user from groups' + ) + return callback(err) + } + return UserGetter.getUserOrUserStubById(user_id, {}, function( + error, + user, + isStub + ) { + if (error) { + return callback(error) + } + if (isStub) { + return callback() + } + return FeaturesUpdater.refreshFeatures(user_id, callback) + }) + }) + }, + + removeUserFromGroup(subscriptionId, user_id, callback) { + return SubscriptionUpdater.removeUserFromGroups( + { _id: subscriptionId }, + user_id, + callback + ) + }, + + removeUserFromAllGroups(user_id, callback) { + return SubscriptionLocator.getMemberSubscriptions(user_id, function( + error, + subscriptions + ) { + if (error) { + return callback(error) + } + if (!subscriptions) { + return callback() + } + const subscriptionIds = subscriptions.map(sub => sub._id) + return SubscriptionUpdater.removeUserFromGroups( + { _id: subscriptionIds }, + user_id, + callback + ) + }) + }, + + deleteWithV1Id(v1TeamId, callback) { + return Subscription.deleteOne({ 'overleaf.id': v1TeamId }, callback) + }, + + deleteSubscription(subscription_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return SubscriptionLocator.getSubscription(subscription_id, function( + err, + subscription + ) { + if (err != null) { + return callback(err) + } + const affected_user_ids = [subscription.admin_id].concat( + subscription.member_ids || [] + ) + logger.log( + { subscription_id, affected_user_ids }, + 'deleting subscription and downgrading users' + ) + return Subscription.remove({ _id: ObjectId(subscription_id) }, function( + err + ) { + if (err != null) { + return callback(err) + } + return async.mapSeries( + affected_user_ids, + FeaturesUpdater.refreshFeatures, + callback + ) + }) + }) + }, + + _createNewSubscription(adminUser_id, callback) { + logger.log({ adminUser_id }, 'creating new subscription') + const subscription = new Subscription({ + admin_id: adminUser_id, + manager_ids: [adminUser_id] + }) + return subscription.save(err => callback(err, subscription)) + }, + + _updateSubscriptionFromRecurly(recurlySubscription, subscription, callback) { + logger.log({ recurlySubscription, subscription }, 'updaing subscription') + if (recurlySubscription.state === 'expired') { + return SubscriptionUpdater.deleteSubscription(subscription._id, callback) + } + subscription.recurlySubscription_id = recurlySubscription.uuid + subscription.planCode = recurlySubscription.plan.plan_code + const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) + if (plan == null) { + return callback( + new Error(`plan code not found: ${subscription.planCode}`) + ) + } + if (plan.groupPlan) { + subscription.groupPlan = true + subscription.membersLimit = plan.membersLimit + } + return subscription.save(function() { + const allIds = _.union(subscription.member_ids, [subscription.admin_id]) + const jobs = allIds.map(user_id => cb => + FeaturesUpdater.refreshFeatures(user_id, cb) + ) + return async.series(jobs, callback) + }) + } +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js new file mode 100644 index 0000000000..c34812d479 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js @@ -0,0 +1,300 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Settings = require('settings-sharelatex') +const RecurlyWrapper = require('./RecurlyWrapper') +const PlansLocator = require('./PlansLocator') +const SubscriptionFormatters = require('./SubscriptionFormatters') +const LimitationsManager = require('./LimitationsManager') +const SubscriptionLocator = require('./SubscriptionLocator') +const V1SubscriptionManager = require('./V1SubscriptionManager') +const InstitutionsGetter = require('../Institutions/InstitutionsGetter') +const PublishersGetter = require('../Publishers/PublishersGetter') +const sanitizeHtml = require('sanitize-html') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const async = require('async') + +const buildBillingDetails = function(recurlySubscription) { + const hostedLoginToken = __guard__( + recurlySubscription != null ? recurlySubscription.account : undefined, + x => x.hosted_login_token + ) + const recurlySubdomain = __guard__( + __guard__(Settings != null ? Settings.apis : undefined, x2 => x2.recurly), + x1 => x1.subdomain + ) + if (hostedLoginToken != null && recurlySubdomain != null) { + return [ + 'https://', + recurlySubdomain, + '.recurly.com/account/billing_info/edit?ht=', + hostedLoginToken + ].join('') + } +} + +module.exports = { + buildUsersSubscriptionViewModel(user, callback) { + if (callback == null) { + callback = function(error, data) {} + } + return async.auto( + { + personalSubscription(cb) { + return SubscriptionLocator.getUsersSubscription(user, cb) + }, + recurlySubscription: [ + 'personalSubscription', + function(cb, { personalSubscription }) { + if ( + (personalSubscription != null + ? personalSubscription.recurlySubscription_id + : undefined) == null || + (personalSubscription != null + ? personalSubscription.recurlySubscription_id + : undefined) === '' + ) { + return cb(null, null) + } + return RecurlyWrapper.getSubscription( + personalSubscription.recurlySubscription_id, + { includeAccount: true }, + cb + ) + } + ], + recurlyCoupons: [ + 'recurlySubscription', + function(cb, { recurlySubscription }) { + if (!recurlySubscription) { + return cb(null, null) + } + const accountId = recurlySubscription.account.account_code + return RecurlyWrapper.getAccountActiveCoupons(accountId, cb) + } + ], + plan: [ + 'personalSubscription', + function(cb, { personalSubscription }) { + if (personalSubscription == null) { + return cb() + } + const plan = PlansLocator.findLocalPlanInSettings( + personalSubscription.planCode + ) + if (plan == null) { + return cb( + new Error( + `No plan found for planCode '${ + personalSubscription.planCode + }'` + ) + ) + } + return cb(null, plan) + } + ], + memberGroupSubscriptions(cb) { + return SubscriptionLocator.getMemberSubscriptions(user, cb) + }, + managedGroupSubscriptions(cb) { + return SubscriptionLocator.getManagedGroupSubscriptions(user, cb) + }, + confirmedMemberInstitutions(cb) { + return InstitutionsGetter.getConfirmedInstitutions(user._id, cb) + }, + managedInstitutions(cb) { + return InstitutionsGetter.getManagedInstitutions(user._id, cb) + }, + managedPublishers(cb) { + return PublishersGetter.getManagedPublishers(user._id, cb) + }, + v1SubscriptionStatus(cb) { + return V1SubscriptionManager.getSubscriptionStatusFromV1( + user._id, + function(error, status, v1Id) { + if (error != null) { + return cb(error) + } + return cb(null, status) + } + ) + } + }, + function(err, results) { + if (err != null) { + return callback(err) + } + let { + personalSubscription, + memberGroupSubscriptions, + managedGroupSubscriptions, + confirmedMemberInstitutions, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus, + recurlySubscription, + recurlyCoupons, + plan + } = results + if (memberGroupSubscriptions == null) { + memberGroupSubscriptions = [] + } + if (managedGroupSubscriptions == null) { + managedGroupSubscriptions = [] + } + if (confirmedMemberInstitutions == null) { + confirmedMemberInstitutions = [] + } + if (managedInstitutions == null) { + managedInstitutions = [] + } + if (v1SubscriptionStatus == null) { + v1SubscriptionStatus = {} + } + if (recurlyCoupons == null) { + recurlyCoupons = [] + } + + if ( + (personalSubscription != null + ? personalSubscription.toObject + : undefined) != null + ) { + // Downgrade from Mongoose object, so we can add a recurly and plan attribute + personalSubscription = personalSubscription.toObject() + } + + if (plan != null) { + personalSubscription.plan = plan + } + + if (personalSubscription != null && recurlySubscription != null) { + const tax = + (recurlySubscription != null + ? recurlySubscription.tax_in_cents + : undefined) || 0 + personalSubscription.recurly = { + tax, + taxRate: parseFloat( + __guard__( + recurlySubscription != null + ? recurlySubscription.tax_rate + : undefined, + x => x._ + ) + ), + billingDetailsLink: buildBillingDetails(recurlySubscription), + price: SubscriptionFormatters.formatPrice( + (recurlySubscription != null + ? recurlySubscription.unit_amount_in_cents + : undefined) + tax, + recurlySubscription != null + ? recurlySubscription.currency + : undefined + ), + nextPaymentDueAt: SubscriptionFormatters.formatDate( + recurlySubscription != null + ? recurlySubscription.current_period_ends_at + : undefined + ), + currency: recurlySubscription.currency, + state: recurlySubscription.state, + trialEndsAtFormatted: SubscriptionFormatters.formatDate( + recurlySubscription != null + ? recurlySubscription.trial_ends_at + : undefined + ), + trial_ends_at: recurlySubscription.trial_ends_at, + activeCoupons: recurlyCoupons + } + } + + for (let memberGroupSubscription of Array.from( + memberGroupSubscriptions + )) { + if (memberGroupSubscription.teamNotice) { + memberGroupSubscription.teamNotice = sanitizeHtml( + memberGroupSubscription.teamNotice + ) + } + } + + return callback(null, { + personalSubscription, + managedGroupSubscriptions, + memberGroupSubscriptions, + confirmedMemberInstitutions, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus + }) + } + ) + }, + + buildViewModel() { + const { plans } = Settings + + const allPlans = {} + plans.forEach(plan => (allPlans[plan.planCode] = plan)) + + const result = { allPlans } + + result.personalAccount = _.find(plans, plan => plan.planCode === 'personal') + + result.studentAccounts = _.filter( + plans, + plan => plan.planCode.indexOf('student') !== -1 + ) + + result.groupMonthlyPlans = _.filter( + plans, + plan => plan.groupPlan && !plan.annual + ) + + result.groupAnnualPlans = _.filter( + plans, + plan => plan.groupPlan && plan.annual + ) + + result.individualMonthlyPlans = _.filter( + plans, + plan => + !plan.groupPlan && + !plan.annual && + plan.planCode !== 'personal' && + plan.planCode.indexOf('student') === -1 + ) + + result.individualAnnualPlans = _.filter( + plans, + plan => + !plan.groupPlan && + plan.annual && + plan.planCode.indexOf('student') === -1 + ) + + return result + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.js b/services/web/app/src/Features/Subscription/TeamInvitesController.js new file mode 100644 index 0000000000..76ef6f4407 --- /dev/null +++ b/services/web/app/src/Features/Subscription/TeamInvitesController.js @@ -0,0 +1,129 @@ +/* eslint-disable + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const TeamInvitesHandler = require('./TeamInvitesHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const SubscriptionLocator = require('./SubscriptionLocator') +const ErrorController = require('../Errors/ErrorController') +const EmailHelper = require('../Helpers/EmailHelper') + +module.exports = { + createInvite(req, res, next) { + const teamManagerId = AuthenticationController.getLoggedInUserId(req) + const subscription = req.entity + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.status(422).json({ + error: { + code: 'invalid_email', + message: req.i18n.translate('invalid_email') + } + }) + } + + return TeamInvitesHandler.createInvite( + teamManagerId, + subscription, + email, + function(err, invite) { + if (err != null) { + return next(err) + } + const inviteView = { + user: { email: invite.email, sentAt: invite.sentAt, invite: true } + } + return res.json(inviteView) + } + ) + }, + + viewInvite(req, res, next) { + const { token } = req.params + const userId = AuthenticationController.getLoggedInUserId(req) + + return TeamInvitesHandler.getInvite(token, function( + err, + invite, + teamSubscription + ) { + if (err != null) { + return next(err) + } + + if (!invite) { + return ErrorController.notFound(req, res, next) + } + + return SubscriptionLocator.getUsersSubscription(userId, function( + err, + personalSubscription + ) { + if (err != null) { + return next(err) + } + + const hasIndividualRecurlySubscription = + personalSubscription != null && + personalSubscription.planCode.match(/(free|trial)/) == null && + personalSubscription.groupPlan === false && + personalSubscription.recurlySubscription_id != null && + personalSubscription.recurlySubscription_id !== '' + + return res.render('subscriptions/team/invite', { + inviterName: invite.inviterName, + inviteToken: invite.token, + hasIndividualRecurlySubscription, + appName: settings.appName + }) + }) + }) + }, + + acceptInvite(req, res, next) { + const { token } = req.params + const userId = AuthenticationController.getLoggedInUserId(req) + + return TeamInvitesHandler.acceptInvite(token, userId, function( + err, + results + ) { + if (err != null) { + return next(err) + } + return res.sendStatus(204) + }) + }, + + revokeInvite(req, res) { + const subscription = req.entity + const email = EmailHelper.parseEmail(req.params.email) + const teamManagerId = AuthenticationController.getLoggedInUserId(req) + if (email == null) { + return res.sendStatus(400) + } + + return TeamInvitesHandler.revokeInvite( + teamManagerId, + subscription, + email, + function(err, results) { + if (err != null) { + return next(err) + } + return res.sendStatus(204) + } + ) + } +} diff --git a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js new file mode 100644 index 0000000000..40b6818c1f --- /dev/null +++ b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js @@ -0,0 +1,288 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TeamInvitesHandler +const logger = require('logger-sharelatex') +const crypto = require('crypto') +const async = require('async') + +const settings = require('settings-sharelatex') +const { ObjectId } = require('mongojs') + +const { TeamInvite } = require('../../models/TeamInvite') +const { Subscription } = require('../../models/Subscription') + +const UserGetter = require('../User/UserGetter') +const SubscriptionLocator = require('./SubscriptionLocator') +const SubscriptionUpdater = require('./SubscriptionUpdater') +const LimitationsManager = require('./LimitationsManager') + +const EmailHandler = require('../Email/EmailHandler') +const EmailHelper = require('../Helpers/EmailHelper') + +const Errors = require('../Errors/Errors') + +module.exports = TeamInvitesHandler = { + getInvite(token, callback) { + return Subscription.findOne({ 'teamInvites.token': token }, function( + err, + subscription + ) { + if (err != null) { + return callback(err) + } + if (subscription == null) { + return callback(new Errors.NotFoundError('team not found')) + } + + const invite = subscription.teamInvites.find(i => i.token === token) + return callback(null, invite, subscription) + }) + }, + + createInvite(teamManagerId, subscription, email, callback) { + email = EmailHelper.parseEmail(email) + if (email == null) { + return callback(new Error('invalid email')) + } + logger.log({ teamManagerId, email }, 'Creating manager team invite') + return UserGetter.getUser(teamManagerId, function(error, teamManager) { + let inviterName + if (error != null) { + return callback(error) + } + + if (teamManager.first_name && teamManager.last_name) { + inviterName = `${teamManager.first_name} ${teamManager.last_name} (${ + teamManager.email + })` + } else { + inviterName = teamManager.email + } + + return removeLegacyInvite(subscription.id, email, function(error) { + if (error != null) { + return callback(error) + } + return createInvite(subscription, email, inviterName, callback) + }) + }) + }, + + importInvite(subscription, inviterName, email, token, sentAt, callback) { + return checkIfInviteIsPossible(subscription, email, function( + error, + possible, + reason + ) { + if (error != null) { + return callback(error) + } + if (!possible) { + return callback(reason) + } + + subscription.teamInvites.push({ + email, + inviterName, + token, + sentAt + }) + + return subscription.save(callback) + }) + }, + + acceptInvite(token, userId, callback) { + logger.log({ userId }, 'Accepting invite') + return TeamInvitesHandler.getInvite(token, function( + err, + invite, + subscription + ) { + if (err != null) { + return callback(err) + } + if (invite == null) { + return callback(new Errors.NotFoundError('invite not found')) + } + + return SubscriptionUpdater.addUserToGroup( + subscription._id, + userId, + function(err) { + if (err != null) { + return callback(err) + } + + return removeInviteFromTeam(subscription.id, invite.email, callback) + } + ) + }) + }, + + revokeInvite(teamManagerId, subscription, email, callback) { + email = EmailHelper.parseEmail(email) + if (email == null) { + return callback(new Error('invalid email')) + } + logger.log({ teamManagerId, email }, 'Revoking invite') + return removeInviteFromTeam(subscription.id, email, callback) + }, + + // Legacy method to allow a user to receive a confirmation email if their + // email is in Subscription.invited_emails when they join. We'll remove this + // after a short while. + createTeamInvitesForLegacyInvitedEmail(email, callback) { + return SubscriptionLocator.getGroupsWithEmailInvite(email, function( + err, + teams + ) { + if (err != null) { + return callback(err) + } + + return async.map( + teams, + (team, cb) => + TeamInvitesHandler.createInvite(team.admin_id, team, email, cb), + callback + ) + }) + } +} + +var createInvite = function(subscription, email, inviterName, callback) { + logger.log( + { subscriptionId: subscription.id, email, inviterName }, + 'Creating invite' + ) + return checkIfInviteIsPossible(subscription, email, function( + error, + possible, + reason + ) { + if (error != null) { + return callback(error) + } + if (!possible) { + return callback(reason) + } + + let invite = subscription.teamInvites.find(invite => invite.email === email) + + if (invite == null) { + invite = { + email, + inviterName, + token: crypto.randomBytes(32).toString('hex'), + sentAt: new Date() + } + subscription.teamInvites.push(invite) + } else { + invite.sentAt = new Date() + } + + return subscription.save(function(error) { + if (error != null) { + return callback(error) + } + + const opts = { + to: email, + inviterName, + acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${ + invite.token + }/`, + appName: settings.appName + } + return EmailHandler.sendEmail('verifyEmailToJoinTeam', opts, error => + callback(error, invite) + ) + }) + }) +} + +var removeInviteFromTeam = function(subscriptionId, email, callback) { + const searchConditions = { _id: new ObjectId(subscriptionId.toString()) } + const removeInvite = { $pull: { teamInvites: { email } } } + logger.log( + { subscriptionId, email, searchConditions, removeInvite }, + 'removeInviteFromTeam' + ) + + return async.series( + [ + cb => Subscription.update(searchConditions, removeInvite, cb), + cb => removeLegacyInvite(subscriptionId, email, cb) + ], + callback + ) +} + +var removeLegacyInvite = (subscriptionId, email, callback) => + Subscription.update( + { + _id: new ObjectId(subscriptionId.toString()) + }, + { + $pull: { + invited_emails: email + } + }, + callback + ) + +var checkIfInviteIsPossible = function(subscription, email, callback) { + if (callback == null) { + callback = function(error, possible, reason) {} + } + if (!subscription.groupPlan) { + logger.log( + { subscriptionId: subscription.id }, + 'can not add members to a subscription that is not in a group plan' + ) + return callback(null, false, { wrongPlan: true }) + } + + if (LimitationsManager.teamHasReachedMemberLimit(subscription)) { + logger.log( + { subscriptionId: subscription.id }, + 'team has reached member limit' + ) + return callback(null, false, { limitReached: true }) + } + + return UserGetter.getUserByAnyEmail(email, function(error, existingUser) { + if (error != null) { + return callback(error) + } + if (existingUser == null) { + return callback(null, true) + } + + const existingMember = subscription.member_ids.find( + memberId => memberId.toString() === existingUser._id.toString() + ) + + if (existingMember) { + logger.log( + { subscriptionId: subscription.id, email }, + 'user already in team' + ) + return callback(null, false, { alreadyInTeam: true }) + } else { + return callback(null, true) + } + }) +} diff --git a/services/web/app/src/Features/Subscription/UserFeaturesUpdater.js b/services/web/app/src/Features/Subscription/UserFeaturesUpdater.js new file mode 100644 index 0000000000..ff379c617f --- /dev/null +++ b/services/web/app/src/Features/Subscription/UserFeaturesUpdater.js @@ -0,0 +1,37 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const logger = require('logger-sharelatex') +const { User } = require('../../models/User') + +module.exports = { + updateFeatures(user_id, features, callback) { + if (callback == null) { + callback = function(err, features, featuresChanged) {} + } + const conditions = { _id: user_id } + const update = {} + logger.log({ user_id, features }, 'updating users features') + for (let key in features) { + const value = features[key] + update[`features.${key}`] = value + } + return User.update(conditions, update, (err, result) => + callback( + err, + features, + (result != null ? result.nModified : undefined) === 1 + ) + ) + } +} diff --git a/services/web/app/src/Features/Subscription/V1SubscriptionManager.js b/services/web/app/src/Features/Subscription/V1SubscriptionManager.js new file mode 100644 index 0000000000..1b3025c490 --- /dev/null +++ b/services/web/app/src/Features/Subscription/V1SubscriptionManager.js @@ -0,0 +1,230 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let V1SubscriptionManager +const UserGetter = require('../User/UserGetter') +const request = require('request') +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const { V1ConnectionError, NotFoundError } = require('../Errors/Errors') + +module.exports = V1SubscriptionManager = { + // Returned planCode = 'v1_pro' | 'v1_pro_plus' | 'v1_student' | 'v1_free' | null + // For this to work, we need plans in settings with plan-codes: + // - 'v1_pro' + // - 'v1_pro_plus' + // - 'v1_student' + // - 'v1_free' + getPlanCodeFromV1(userId, callback) { + if (callback == null) { + callback = function(err, planCode, v1Id) {} + } + logger.log({ userId }, '[V1SubscriptionManager] fetching v1 plan for user') + return V1SubscriptionManager._v1Request( + userId, + { + method: 'GET', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/plan_code` + } + }, + function(error, body, v1Id) { + if (error != null) { + return callback(error) + } + let planName = body != null ? body.plan_name : undefined + logger.log( + { userId, planName, body }, + '[V1SubscriptionManager] fetched v1 plan for user' + ) + if (['pro', 'pro_plus', 'student', 'free'].includes(planName)) { + planName = `v1_${planName}` + } else { + // Throw away 'anonymous', etc as being equivalent to null + planName = null + } + return callback(null, planName, v1Id) + } + ) + }, + + notifyV1OfFeaturesChange(userId, callback) { + if (callback == null) { + callback = function(error) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'POST', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/sync` + } + }, + callback + ) + }, + + getSubscriptionsFromV1(userId, callback) { + if (callback == null) { + callback = function(err, subscriptions, v1Id) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'GET', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/subscriptions` + } + }, + callback + ) + }, + + getSubscriptionStatusFromV1(userId, callback) { + if (callback == null) { + callback = function(err, status) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'GET', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/subscription_status` + } + }, + callback + ) + }, + + cancelV1Subscription(userId, callback) { + if (callback == null) { + callback = function(err) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'DELETE', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/subscription` + } + }, + callback + ) + }, + + v1IdForUser(userId, callback) { + if (callback == null) { + callback = function(err, v1Id) {} + } + return UserGetter.getUser(userId, { 'overleaf.id': 1 }, function( + err, + user + ) { + if (err != null) { + return callback(err) + } + const v1Id = __guard__( + user != null ? user.overleaf : undefined, + x => x.id + ) + if (v1Id == null) { + logger.log( + { userId }, + '[V1SubscriptionManager] no v1 id found for user' + ) + } + + return callback(null, v1Id) + }) + }, + + // v1 accounts created before migration to v2 had github and mendeley for free + // but these are now paid-for features for new accounts (v1id > cutoff) + getGrandfatheredFeaturesForV1User(v1Id) { + const cutoff = settings.v1GrandfatheredFeaturesUidCutoff + if (cutoff == null) { + return {} + } + if (v1Id == null) { + return {} + } + + if (v1Id < cutoff) { + return settings.v1GrandfatheredFeatures || {} + } else { + return {} + } + }, + + _v1Request(userId, options, callback) { + if (callback == null) { + callback = function(err, body, v1Id) {} + } + if (!__guard__(settings != null ? settings.apis : undefined, x => x.v1)) { + return callback(null, null) + } + + return V1SubscriptionManager.v1IdForUser(userId, function(err, v1Id) { + if (err != null) { + return callback(err) + } + if (v1Id == null) { + return callback(null, null, null) + } + return request( + { + baseUrl: settings.apis.v1.url, + url: options.url(v1Id), + method: options.method, + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + sendImmediately: true + }, + json: true, + timeout: 15 * 1000 + }, + function(error, response, body) { + if (error != null) { + // Specially handle no connection err, so warning can be shown + if (error.code === 'ECONNREFUSED') { + error = new V1ConnectionError('No V1 connection') + } + return callback(error) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + return callback(null, body, v1Id) + } else { + if (response.statusCode === 404) { + return callback(new NotFoundError(`v1 user not found: ${userId}`)) + } else { + return callback( + new Error( + `non-success code from v1: ${response.statusCode} ${ + options.method + } ${options.url(v1Id)}` + ) + ) + } + } + } + ) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Subscription/planFeatures.js b/services/web/app/src/Features/Subscription/planFeatures.js new file mode 100644 index 0000000000..071c5d47ba --- /dev/null +++ b/services/web/app/src/Features/Subscription/planFeatures.js @@ -0,0 +1,134 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +module.exports = [ + { + feature: 'number_collab', + value: 'str', + plans: { + free: '1', + coll: '10', + prof: 'unlimited' + }, + student: '6' + }, + { + feature: 'unlimited_private', + value: 'bool', + info: 'unlimited_private_info', + plans: { + free: true, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'realtime_collab', + value: 'bool', + info: 'realtime_collab_info', + plans: { + free: true, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'thousands_templates', + value: 'bool', + info: 'hundreds_templates_info', + plans: { + free: true, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'powerful_latex_editor', + value: 'bool', + info: 'latex_editor_info', + plans: { + free: true, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'realtime_track_changes', + value: 'bool', + info: 'realtime_track_changes_info', + plans: { + free: false, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'reference_search', + value: 'bool', + info: 'reference_search_info', + plans: { + free: false, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'reference_sync', + info: 'reference_sync_info', + value: 'bool', + plans: { + free: false, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'full_doc_history', + value: 'bool', + info: 'full_doc_history_info', + plans: { + free: false, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'dropbox_integration_lowercase', + value: 'bool', + info: 'dropbox_integration_info', + plans: { + free: false, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'github_integration_lowercase', + value: 'bool', + info: 'github_integration_info', + plans: { + free: false, + coll: true, + prof: true + }, + student: true + }, + { + feature: 'priority_support', + value: 'bool', + plans: { + free: false, + coll: true, + prof: true + }, + student: true + } +] diff --git a/services/web/app/src/Features/SudoMode/SudoModeController.js b/services/web/app/src/Features/SudoMode/SudoModeController.js new file mode 100644 index 0000000000..1b6a0f7294 --- /dev/null +++ b/services/web/app/src/Features/SudoMode/SudoModeController.js @@ -0,0 +1,114 @@ +/* eslint-disable + max-len, + no-unused-vars, + no-use-before-define, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SudoModeController +const logger = require('logger-sharelatex') +const SudoModeHandler = require('./SudoModeHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const { ObjectId } = require('../../infrastructure/Mongoose').mongo +const UserGetter = require('../User/UserGetter') +const Settings = require('settings-sharelatex') + +module.exports = SudoModeController = { + sudoModePrompt(req, res, next) { + if (req.externalAuthenticationSystemUsed() && Settings.overleaf == null) { + logger.log({ userId }, '[SudoMode] using external auth, redirecting') + return res.redirect('/project') + } + var userId = AuthenticationController.getLoggedInUserId(req) + logger.log({ userId }, '[SudoMode] rendering sudo mode password page') + return SudoModeHandler.isSudoModeActive(userId, function(err, isActive) { + if (err != null) { + logger.err( + { err, userId }, + '[SudoMode] error checking if sudo mode is active' + ) + return next(err) + } + if (isActive) { + logger.log( + { userId }, + '[SudoMode] sudo mode already active, redirecting' + ) + return res.redirect('/project') + } + return res.render('sudo_mode/sudo_mode_prompt', { + title: 'confirm_password_to_continue' + }) + }) + }, + + submitPassword(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const redir = + AuthenticationController._getRedirectFromSession(req) || '/project' + const { password } = req.body + if (!password) { + logger.log( + { userId }, + '[SudoMode] no password supplied, failed authentication' + ) + return next(new Error('no password supplied')) + } + logger.log({ userId, redir }, '[SudoMode] checking user password') + return UserGetter.getUser(ObjectId(userId), { email: 1 }, function( + err, + userRecord + ) { + if (err != null) { + logger.err({ err, userId }, '[SudoMode] error getting user') + return next(err) + } + if (userRecord == null) { + err = new Error('user not found') + logger.err({ err, userId }, '[SudoMode] user not found') + return next(err) + } + return SudoModeHandler.authenticate(userRecord.email, password, function( + err, + user + ) { + if (err != null) { + logger.err({ err, userId }, '[SudoMode] error authenticating user') + return next(err) + } + if (user != null) { + logger.log( + { userId }, + '[SudoMode] authenticated user, activating sudo mode' + ) + return SudoModeHandler.activateSudoMode(userId, function(err) { + if (err != null) { + logger.err( + { err, userId }, + '[SudoMode] error activating sudo mode' + ) + return next(err) + } + return res.json({ + redir + }) + }) + } else { + logger.log({ userId }, '[SudoMode] authentication failed for user') + return res.json({ + message: { + text: req.i18n.translate('invalid_password'), + type: 'error' + } + }) + } + }) + }) + } +} diff --git a/services/web/app/src/Features/SudoMode/SudoModeHandler.js b/services/web/app/src/Features/SudoMode/SudoModeHandler.js new file mode 100644 index 0000000000..282d1c4cc9 --- /dev/null +++ b/services/web/app/src/Features/SudoMode/SudoModeHandler.js @@ -0,0 +1,95 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SudoModeHandler +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('sudomode') +const logger = require('logger-sharelatex') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const Settings = require('settings-sharelatex') +const V1Handler = require('../V1/V1Handler') +const UserGetter = require('../User/UserGetter') + +const TIMEOUT_IN_SECONDS = 60 * 60 + +module.exports = SudoModeHandler = { + _buildKey(userId) { + return `SudoMode:{${userId}}` + }, + + authenticate(email, password, callback) { + if (callback == null) { + callback = function(err, user) {} + } + if (Settings.overleaf != null) { + return V1Handler.authWithV1(email, password, function( + err, + isValid, + v1Profile + ) { + if (!isValid) { + return callback(null, null) + } + return UserGetter.getUser({ 'overleaf.id': v1Profile.id }, callback) + }) + } else { + return AuthenticationManager.authenticate({ email }, password, callback) + } + }, + + activateSudoMode(userId, callback) { + if (callback == null) { + callback = function(err) {} + } + if (userId == null) { + return callback(new Error('[SudoMode] user must be supplied')) + } + const duration = TIMEOUT_IN_SECONDS + logger.log({ userId, duration }, '[SudoMode] activating sudo mode for user') + return rclient.set( + SudoModeHandler._buildKey(userId), + '1', + 'EX', + duration, + callback + ) + }, + + clearSudoMode(userId, callback) { + if (callback == null) { + callback = function(err) {} + } + if (userId == null) { + return callback(new Error('[SudoMode] user must be supplied')) + } + logger.log({ userId }, '[SudoMode] clearing sudo mode for user') + return rclient.del(SudoModeHandler._buildKey(userId), callback) + }, + + isSudoModeActive(userId, callback) { + if (callback == null) { + callback = function(err, isActive) {} + } + if (userId == null) { + return callback(new Error('[SudoMode] user must be supplied')) + } + return rclient.get(SudoModeHandler._buildKey(userId), function( + err, + result + ) { + if (err != null) { + return callback(err) + } + return callback(null, result === '1') + }) + } +} diff --git a/services/web/app/src/Features/SudoMode/SudoModeMiddleware.js b/services/web/app/src/Features/SudoMode/SudoModeMiddleware.js new file mode 100644 index 0000000000..67a52f139b --- /dev/null +++ b/services/web/app/src/Features/SudoMode/SudoModeMiddleware.js @@ -0,0 +1,52 @@ +/* eslint-disable + max-len, + no-unused-vars, + no-use-before-define, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SudoModeMiddleware +const logger = require('logger-sharelatex') +const SudoModeHandler = require('./SudoModeHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const Settings = require('settings-sharelatex') + +module.exports = SudoModeMiddleware = { + protectPage(req, res, next) { + if (req.externalAuthenticationSystemUsed() && Settings.overleaf == null) { + logger.log( + { userId }, + '[SudoMode] using external auth, skipping sudo-mode check' + ) + return next() + } + var userId = AuthenticationController.getLoggedInUserId(req) + logger.log( + { userId }, + '[SudoMode] protecting endpoint, checking if sudo mode is active' + ) + return SudoModeHandler.isSudoModeActive(userId, function(err, isActive) { + if (err != null) { + logger.err( + { err, userId }, + '[SudoMode] error checking if sudo mode is active' + ) + return next(err) + } + if (isActive) { + logger.log({ userId }, '[SudoMode] sudo mode active, continuing') + return next() + } else { + logger.log({ userId }, '[SudoMode] sudo mode not active, redirecting') + AuthenticationController.setRedirectInSession(req) + return res.redirect('/confirm-password') + } + }) + } +} diff --git a/services/web/app/src/Features/SystemMessages/SystemMessageManager.js b/services/web/app/src/Features/SystemMessages/SystemMessageManager.js new file mode 100644 index 0000000000..7dda788764 --- /dev/null +++ b/services/web/app/src/Features/SystemMessages/SystemMessageManager.js @@ -0,0 +1,61 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SystemMessageManager +const { SystemMessage } = require('../../models/SystemMessage') + +module.exports = SystemMessageManager = { + getMessages(callback) { + if (callback == null) { + callback = function(error, messages) {} + } + if (this._cachedMessages != null) { + return callback(null, this._cachedMessages) + } else { + return this.getMessagesFromDB((error, messages) => { + if (error != null) { + return callback(error) + } + this._cachedMessages = messages + return callback(null, messages) + }) + } + }, + + getMessagesFromDB(callback) { + if (callback == null) { + callback = function(error, messages) {} + } + return SystemMessage.find({}, callback) + }, + + clearMessages(callback) { + if (callback == null) { + callback = function(error) {} + } + return SystemMessage.remove({}, callback) + }, + + createMessage(content, callback) { + if (callback == null) { + callback = function(error) {} + } + const message = new SystemMessage({ content }) + return message.save(callback) + }, + + clearCache() { + return delete this._cachedMessages + } +} + +const CACHE_TIMEOUT = 20 * 1000 // 20 seconds +setInterval(() => SystemMessageManager.clearCache(), CACHE_TIMEOUT) diff --git a/services/web/app/src/Features/Tags/TagsController.js b/services/web/app/src/Features/Tags/TagsController.js new file mode 100644 index 0000000000..4f5028fc1a --- /dev/null +++ b/services/web/app/src/Features/Tags/TagsController.js @@ -0,0 +1,100 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const TagsHandler = require('./TagsHandler') +const logger = require('logger-sharelatex') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = { + getAllTags(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ user_id }, 'getting tags') + return TagsHandler.getAllTags(user_id, function(error, allTags) { + if (error != null) { + return next(error) + } + return res.json(allTags) + }) + }, + + createTag(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const { name } = req.body + logger.log({ user_id, name }, 'creating tag') + return TagsHandler.createTag(user_id, name, function(error, tag) { + if (error != null) { + return next(error) + } + return res.json(tag) + }) + }, + + addProjectToTag(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const { tag_id, project_id } = req.params + logger.log({ user_id, tag_id, project_id }, 'adding tag to project') + return TagsHandler.addProjectToTag(user_id, tag_id, project_id, function( + error + ) { + if (error != null) { + return next(error) + } + return res.status(204).end() + }) + }, + + removeProjectFromTag(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const { tag_id, project_id } = req.params + logger.log({ user_id, tag_id, project_id }, 'removing tag from project') + return TagsHandler.removeProjectFromTag( + user_id, + tag_id, + project_id, + function(error) { + if (error != null) { + return next(error) + } + return res.status(204).end() + } + ) + }, + + deleteTag(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const { tag_id } = req.params + logger.log({ user_id, tag_id }, 'deleting tag') + return TagsHandler.deleteTag(user_id, tag_id, function(error) { + if (error != null) { + return next(error) + } + return res.status(204).end() + }) + }, + + renameTag(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const { tag_id } = req.params + const name = req.body != null ? req.body.name : undefined + if (name == null) { + return res.status(400).end() + } else { + logger.log({ user_id, tag_id, name }, 'renaming tag') + return TagsHandler.renameTag(user_id, tag_id, name, function(error) { + if (error != null) { + return next(error) + } + return res.status(204).end() + }) + } + } +} diff --git a/services/web/app/src/Features/Tags/TagsHandler.js b/services/web/app/src/Features/Tags/TagsHandler.js new file mode 100644 index 0000000000..91fa4e392e --- /dev/null +++ b/services/web/app/src/Features/Tags/TagsHandler.js @@ -0,0 +1,230 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TagsHandler +const _ = require('underscore') +const settings = require('settings-sharelatex') +const request = require('request') +const logger = require('logger-sharelatex') + +const TIMEOUT = 1000 +module.exports = TagsHandler = { + getAllTags(user_id, callback) { + return this._requestTags(user_id, (err, allTags) => { + if (allTags == null) { + allTags = [] + } + return this._groupTagsByProject(allTags, function(err, groupedByProject) { + logger.log( + { allTags, user_id, groupedByProject }, + 'got all tags from tags api' + ) + return callback(err, allTags, groupedByProject) + }) + }) + }, + + createTag(user_id, name, callback) { + if (callback == null) { + callback = function(error, tag) {} + } + const opts = { + url: `${settings.apis.tags.url}/user/${user_id}/tag`, + json: { + name + }, + timeout: TIMEOUT + } + return request.post(opts, (err, res, body) => + TagsHandler._handleResponse(err, res, { user_id }, function(error) { + if (error != null) { + return callback(error) + } + return callback(null, body || {}) + }) + ) + }, + + renameTag(user_id, tag_id, name, callback) { + if (callback == null) { + callback = function(error) {} + } + const url = `${settings.apis.tags.url}/user/${user_id}/tag/${tag_id}/rename` + return request.post( + { + url, + json: { + name + }, + timeout: TIMEOUT + }, + (err, res, body) => + TagsHandler._handleResponse( + err, + res, + { url, user_id, tag_id, name }, + callback + ) + ) + }, + + deleteTag(user_id, tag_id, callback) { + if (callback == null) { + callback = function(error) {} + } + const url = `${settings.apis.tags.url}/user/${user_id}/tag/${tag_id}` + return request.del({ url, timeout: TIMEOUT }, (err, res, body) => + TagsHandler._handleResponse(err, res, { url, user_id, tag_id }, callback) + ) + }, + + updateTagUserIds(old_user_id, new_user_id, callback) { + const opts = { + url: `${settings.apis.tags.url}/user/${old_user_id}/tag`, + json: { + user_id: new_user_id + }, + timeout: TIMEOUT + } + return request.put(opts, (err, res, body) => + TagsHandler._handleResponse( + err, + res, + { old_user_id, new_user_id }, + callback + ) + ) + }, + + removeProjectFromTag(user_id, tag_id, project_id, callback) { + const url = `${ + settings.apis.tags.url + }/user/${user_id}/tag/${tag_id}/project/${project_id}` + return request.del({ url, timeout: TIMEOUT }, (err, res, body) => + TagsHandler._handleResponse( + err, + res, + { url, user_id, tag_id, project_id }, + callback + ) + ) + }, + + addProjectToTag(user_id, tag_id, project_id, callback) { + const url = `${ + settings.apis.tags.url + }/user/${user_id}/tag/${tag_id}/project/${project_id}` + return request.post({ url, timeout: TIMEOUT }, (err, res, body) => + TagsHandler._handleResponse( + err, + res, + { url, user_id, tag_id, project_id }, + callback + ) + ) + }, + + addProjectToTagName(user_id, name, project_id, callback) { + const url = `${ + settings.apis.tags.url + }/user/${user_id}/tag/project/${project_id}` + const opts = { + json: { name }, + timeout: TIMEOUT, + url + } + return request.post(opts, (err, res, body) => + TagsHandler._handleResponse( + err, + res, + { url, user_id, name, project_id }, + callback + ) + ) + }, + + removeProjectFromAllTags(user_id, project_id, callback) { + const url = `${ + settings.apis.tags.url + }/user/${user_id}/project/${project_id}` + const opts = { + url, + timeout: TIMEOUT + } + return request.del(opts, (err, res, body) => + TagsHandler._handleResponse( + err, + res, + { url, user_id, project_id }, + callback + ) + ) + }, + + _handleResponse(err, res, params, callback) { + if (err != null) { + params.err = err + logger.err(params, 'error in tag api') + return callback(err) + } else if (res != null && res.statusCode >= 200 && res.statusCode < 300) { + return callback(null) + } else { + err = new Error( + `tags api returned a failure status code: ${ + res != null ? res.statusCode : undefined + }` + ) + params.err = err + logger.err( + params, + `tags api returned failure status code: ${ + res != null ? res.statusCode : undefined + }` + ) + return callback(err) + } + }, + + _requestTags(user_id, callback) { + const opts = { + url: `${settings.apis.tags.url}/user/${user_id}/tag`, + json: true, + timeout: TIMEOUT + } + return request.get(opts, (err, res, body) => + TagsHandler._handleResponse(err, res, { user_id }, function(error) { + if (error != null) { + return callback(error, []) + } + return callback(null, body || []) + }) + ) + }, + + _groupTagsByProject(tags, callback) { + const result = {} + _.each(tags, tag => + _.each(tag.project_ids, project_id => (result[project_id] = [])) + ) + + _.each(tags, tag => + _.each(tag.project_ids, function(project_id) { + const clonedTag = _.clone(tag) + delete clonedTag.project_ids + return result[project_id].push(clonedTag) + }) + ) + return callback(null, result) + } +} diff --git a/services/web/app/src/Features/Templates/TemplatesController.js b/services/web/app/src/Features/Templates/TemplatesController.js new file mode 100644 index 0000000000..853c1f53f2 --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesController.js @@ -0,0 +1,69 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TemplatesController +const path = require('path') +const AuthenticationController = require('../Authentication/AuthenticationController') +const TemplatesManager = require('./TemplatesManager') +const ProjectHelper = require('../Project/ProjectHelper') +const logger = require('logger-sharelatex') + +module.exports = TemplatesController = { + getV1Template(req, res) { + const templateVersionId = req.params.Template_version_id + const templateId = req.query.id + if (!/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId)) { + logger.err( + { templateVersionId, templateId }, + 'invalid template id or version' + ) + return res.sendStatus(400) + } + const data = {} + data.templateVersionId = templateVersionId + data.templateId = templateId + data.name = req.query.templateName + data.compiler = ProjectHelper.compilerFromV1Engine(req.query.latexEngine) + data.imageName = req.query.texImage + data.mainFile = req.query.mainFile + data.brandVariationId = req.query.brandVariationId + return res.render( + path.resolve( + __dirname, + '../../../views/project/editor/new_from_template' + ), + data + ) + }, + + createProjectFromV1Template(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return TemplatesManager.createProjectFromV1Template( + req.body.brandVariationId, + req.body.compiler, + req.body.mainFile, + req.body.templateId, + req.body.templateName, + req.body.templateVersionId, + user_id, + req.body.imageName, + function(err, project) { + if (err != null) { + return next(err) + } + delete req.session.templateData + return res.redirect(`/project/${project._id}`) + } + ) + } +} diff --git a/services/web/app/src/Features/Templates/TemplatesManager.js b/services/web/app/src/Features/Templates/TemplatesManager.js new file mode 100644 index 0000000000..99e966c666 --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesManager.js @@ -0,0 +1,157 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TemplatesManager +const { Project } = require('../../models/Project') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const ProjectUploadManager = require('../Uploads/ProjectUploadManager') +const FileWriter = require('../../infrastructure/FileWriter') +const async = require('async') +const fs = require('fs') +const logger = require('logger-sharelatex') +const request = require('request') +const settings = require('settings-sharelatex') +const uuid = require('uuid') + +module.exports = TemplatesManager = { + createProjectFromV1Template( + brandVariationId, + compiler, + mainFile, + templateId, + templateName, + templateVersionId, + user_id, + imageName, + callback + ) { + const zipUrl = `${ + settings.apis.v1.url + }/api/v1/sharelatex/templates/${templateVersionId}` + const zipReq = request(zipUrl, { + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass + } + }) + zipReq.on('error', function(err) { + logger.error({ err }, 'error getting zip from template API') + return callback(err) + }) + return FileWriter.ensureDumpFolderExists(function(err) { + if (err != null) { + return callback(err) + } + + const projectName = ProjectDetailsHandler.fixProjectName(templateName) + const dumpPath = `${settings.path.dumpFolder}/${uuid.v4()}` + const writeStream = fs.createWriteStream(dumpPath) + writeStream.on('close', function() { + if (zipReq.response.statusCode !== 200) { + logger.err( + { uri: zipUrl, statusCode: zipReq.response.statusCode }, + 'non-success code getting zip from template API' + ) + return callback(new Error('get zip failed')) + } + return ProjectUploadManager.createProjectFromZipArchiveWithName( + user_id, + projectName, + dumpPath, + function(err, project) { + if (err != null) { + logger.err({ err, zipReq }, 'problem building project from zip') + return callback(err) + } + return async.series( + [ + cb => TemplatesManager._setCompiler(project._id, compiler, cb), + cb => TemplatesManager._setImage(project._id, imageName, cb), + cb => TemplatesManager._setMainFile(project._id, mainFile, cb), + cb => + TemplatesManager._setBrandVariationId( + project._id, + brandVariationId, + cb + ) + ], + function(err) { + if (err != null) { + return callback(err) + } + fs.unlink(dumpPath, function(err) { + if (err != null) { + return logger.err({ err }, 'error unlinking template zip') + } + }) + const update = { + fromV1TemplateId: templateId, + fromV1TemplateVersionId: templateVersionId + } + return Project.update( + { _id: project._id }, + update, + {}, + function(err) { + if (err != null) { + return callback(err) + } + return callback(null, project) + } + ) + } + ) + } + ) + }) + return zipReq.pipe(writeStream) + }) + }, + + _setCompiler(project_id, compiler, callback) { + if (compiler == null) { + return callback() + } + return ProjectOptionsHandler.setCompiler(project_id, compiler, callback) + }, + + _setImage(project_id, imageName, callback) { + if (!imageName) { + imageName = 'wl_texlive:2018.1' + } + return ProjectOptionsHandler.setImageName(project_id, imageName, callback) + }, + + _setMainFile(project_id, mainFile, callback) { + if (mainFile == null) { + return callback() + } + return ProjectRootDocManager.setRootDocFromName( + project_id, + mainFile, + callback + ) + }, + + _setBrandVariationId(project_id, brandVariationId, callback) { + if (brandVariationId == null) { + return callback() + } + return ProjectOptionsHandler.setBrandVariationId( + project_id, + brandVariationId, + callback + ) + } +} diff --git a/services/web/app/src/Features/Templates/TemplatesMiddleware.js b/services/web/app/src/Features/Templates/TemplatesMiddleware.js new file mode 100644 index 0000000000..5a266dfb84 --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesMiddleware.js @@ -0,0 +1,21 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') + +module.exports = { + saveTemplateDataInSession(req, res, next) { + if (req.query.templateName) { + req.session.templateData = req.query + } + return next() + } +} diff --git a/services/web/app/src/Features/Templates/TemplatesRouter.js b/services/web/app/src/Features/Templates/TemplatesRouter.js new file mode 100644 index 0000000000..be3e164c2d --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesRouter.js @@ -0,0 +1,36 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthenticationController = require('../Authentication/AuthenticationController') +const TemplatesController = require('./TemplatesController') +const TemplatesMiddleware = require('./TemplatesMiddleware') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') + +module.exports = { + apply(app) { + app.get( + '/project/new/template/:Template_version_id', + TemplatesMiddleware.saveTemplateDataInSession, + AuthenticationController.requireLogin(), + TemplatesController.getV1Template + ) + + return app.post( + '/project/new/template', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-project-from-template', + maxRequests: 20, + timeInterval: 60 + }), + TemplatesController.createProjectFromV1Template + ) + } +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js new file mode 100644 index 0000000000..effafd4e81 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js @@ -0,0 +1,157 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let parseParams +const tpdsUpdateHandler = require('./TpdsUpdateHandler') +const UpdateMerger = require('./UpdateMerger') +const logger = require('logger-sharelatex') +const Path = require('path') +const metrics = require('metrics-sharelatex') + +module.exports = { + // mergeUpdate and deleteUpdate are used by Dropbox, where the project is only passed as the name, as the + // first part of the file path. They have to check the project exists, find it, and create it if not. + // They also ignore 'noisy' files like .DS_Store, .gitignore, etc. + mergeUpdate(req, res) { + metrics.inc('tpds.merge-update') + const { filePath, user_id, projectName } = parseParams(req) + const source = req.headers['x-sl-update-source'] || 'unknown' + logger.log( + { user_id, filePath, fullPath: req.params[0], projectName, source }, + 'reciving update request from tpds' + ) + return tpdsUpdateHandler.newUpdate( + user_id, + projectName, + filePath, + req, + source, + function(err) { + logger.log( + { user_id, filePath, fullPath: req.params[0] }, + 'sending response that tpdsUpdate has been completed' + ) + if (err != null) { + logger.err( + { err, user_id, filePath }, + 'error reciving update from tpds' + ) + return res.sendStatus(500) + } else { + logger.log( + { user_id, filePath, projectName }, + 'telling tpds update has been processed' + ) + return res.sendStatus(200) + } + } + ) + }, + + deleteUpdate(req, res) { + metrics.inc('tpds.delete-update') + const { filePath, user_id, projectName } = parseParams(req) + const source = req.headers['x-sl-update-source'] || 'unknown' + logger.log( + { user_id, filePath, projectName, fullPath: req.params[0], source }, + 'reciving delete request from tpds' + ) + return tpdsUpdateHandler.deleteUpdate( + user_id, + projectName, + filePath, + source, + function(err) { + if (err != null) { + logger.err( + { err, user_id, filePath }, + 'error reciving update from tpds' + ) + return res.sendStatus(500) + } else { + logger.log( + { user_id, filePath, projectName }, + 'telling tpds delete has been processed' + ) + return res.sendStatus(200) + } + } + ) + }, + + // updateProjectContents and deleteProjectContents are used by GitHub. The project_id is known so we + // can skip right ahead to creating/updating/deleting the file. These methods will not ignore noisy + // files like .DS_Store, .gitignore, etc because people are generally more explicit with the files they + // want in git. + updateProjectContents(req, res, next) { + if (next == null) { + next = function(error) {} + } + const { project_id } = req.params + const path = `/${req.params[0]}` // UpdateMerger expects leading slash + const source = req.headers['x-sl-update-source'] || 'unknown' + logger.log({ project_id, path, source }, 'received project contents update') + return UpdateMerger.mergeUpdate( + null, + project_id, + path, + req, + source, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + } + ) + }, + + deleteProjectContents(req, res, next) { + if (next == null) { + next = function(error) {} + } + const { project_id } = req.params + const path = `/${req.params[0]}` // UpdateMerger expects leading slash + const source = req.headers['x-sl-update-source'] || 'unknown' + logger.log( + { project_id, path, source }, + 'received project contents delete request' + ) + return UpdateMerger.deleteUpdate(null, project_id, path, source, function( + error + ) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + }, + + parseParams: (parseParams = function(req) { + let filePath, projectName + let path = req.params[0] + const { user_id } = req.params + + path = Path.join('/', path) + if (path.substring(1).indexOf('/') === -1) { + filePath = '/' + projectName = path.substring(1) + } else { + filePath = path.substring(path.indexOf('/', 1)) + projectName = path.substring(0, path.indexOf('/', 1)) + projectName = projectName.replace('/', '') + } + + return { filePath, user_id, projectName } + }) +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js new file mode 100644 index 0000000000..371a30c521 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js @@ -0,0 +1,131 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const updateMerger = require('./UpdateMerger') +const logger = require('logger-sharelatex') +const projectLocator = require('../Project/ProjectLocator') +const projectCreationHandler = require('../Project/ProjectCreationHandler') +const projectDeleter = require('../Project/ProjectDeleter') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const FileTypeManager = require('../Uploads/FileTypeManager') +const CooldownManager = require('../Cooldown/CooldownManager') +const Errors = require('../Errors/Errors') + +const commitMessage = 'Before update from Dropbox' + +module.exports = { + newUpdate(user_id, projectName, path, updateRequest, source, callback) { + const getOrCreateProject = cb => { + return projectLocator.findUsersProjectByName( + user_id, + projectName, + (err, project) => { + logger.log( + { user_id, filePath: path, projectName }, + 'handling new update from tpds' + ) + if (project == null) { + return projectCreationHandler.createBlankProject( + user_id, + projectName, + (err, project) => { + // have a crack at setting the root doc after a while, on creation we won't have it yet, but should have + // been sent it it within 30 seconds + setTimeout( + () => + ProjectRootDocManager.setRootDocAutomatically(project._id), + this._rootDocTimeoutLength + ) + return cb(err, project) + } + ) + } else { + return cb(err, project) + } + } + ) + } + return getOrCreateProject(function(err, project) { + if (err != null) { + return callback(err) + } + return CooldownManager.isProjectOnCooldown(project._id, function( + err, + projectIsOnCooldown + ) { + if (err != null) { + return callback(err) + } + if (projectIsOnCooldown) { + logger.log( + { projectId: project._id }, + 'project is on cooldown, denying request' + ) + return callback( + new Errors.TooManyRequestsError('project on cooldown') + ) + } + return FileTypeManager.shouldIgnore(path, function(err, shouldIgnore) { + if (shouldIgnore) { + return callback() + } + return updateMerger.mergeUpdate( + user_id, + project._id, + path, + updateRequest, + source, + callback + ) + }) + }) + }) + }, + + deleteUpdate(user_id, projectName, path, source, callback) { + logger.log({ user_id, filePath: path }, 'handling delete update from tpds') + return projectLocator.findUsersProjectByName(user_id, projectName, function( + err, + project + ) { + if (project == null) { + logger.log( + { user_id, filePath: path, projectName }, + 'project not found from tpds update, ignoring folder or project' + ) + return callback() + } + if (path === '/') { + logger.log( + { user_id, filePath: path, projectName, project_id: project._id }, + 'project found for delete update, path is root so marking project as deleted' + ) + return projectDeleter.markAsDeletedByExternalSource( + project._id, + callback + ) + } else { + return updateMerger.deleteUpdate( + user_id, + project._id, + path, + source, + err => callback(err) + ) + } + }) + }, + + _rootDocTimeoutLength: 30 * 1000 +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js new file mode 100644 index 0000000000..60a8247113 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js @@ -0,0 +1,323 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-irregular-whitespace, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TpdsUpdateSender, tpdsUrl +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const path = require('path') +const ProjectGetter = require('../Project/ProjectGetter') +const keys = require('../../infrastructure/Keys') +const metrics = require('metrics-sharelatex') +const request = require('request') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') + +const buildPath = function(user_id, project_name, filePath) { + let projectPath = path.join(project_name, '/', filePath) + projectPath = encodeURIComponent(projectPath) + const fullPath = path.join('/user/', `${user_id}`, '/entity/', projectPath) + return fullPath +} + +const tpdsworkerEnabled = () => + (settings.apis.tpdsworker != null + ? settings.apis.tpdsworker.url + : undefined) != null +if (!tpdsworkerEnabled()) { + logger.log('tpdsworker is not enabled, request will not be sent to it') +} + +if (settings.apis.thirdPartyDataStore.linode_url != null) { + tpdsUrl = settings.apis.thirdPartyDataStore.linode_url +} else { + tpdsUrl = settings.apis.thirdPartyDataStore.url +} + +module.exports = TpdsUpdateSender = { + _enqueue(group, method, job, callback) { + if (!tpdsworkerEnabled()) { + return callback() + } + const opts = { + uri: `${settings.apis.tpdsworker.url}/enqueue/web_to_tpds_http_requests`, + json: { + group, + method, + job + }, + method: 'post', + timeout: 5 * 1000 + } + return request(opts, function(err) { + if (err != null) { + logger.err( + { err }, + 'error queuing something in the tpdsworker, continuing anyway' + ) + return callback() + } else { + logger.log({ group, job }, 'successfully queued up job for tpdsworker') + return callback() + } + }) + }, + + _addEntity(options, callback) { + if (callback == null) { + callback = function(err) {} + } + return getProjectsUsersIds(options.project_id, function( + err, + user_id, + allUserIds + ) { + if (err != null) { + logger.err({ err, options }, 'error getting projects user ids') + return callback(err) + } + logger.log( + { + project_id: options.project_id, + user_id, + path: options.path, + uri: options.uri, + rev: options.rev + }, + 'sending file to third party data store' + ) + const postOptions = { + method: 'post', + headers: { + sl_entity_rev: options.rev, + sl_project_id: options.project_id, + sl_all_user_ids: JSON.stringify(allUserIds) + }, + uri: `${tpdsUrl}${buildPath( + user_id, + options.project_name, + options.path + )}`, + title: 'addFile', + streamOrigin: options.streamOrigin + } + return TpdsUpdateSender._enqueue( + options.project_id, + 'pipeStreamFrom', + postOptions, + function(err) { + if (err != null) { + logger.err( + { + err, + project_id: options.project_id, + user_id, + path: options.path, + uri: options.uri, + rev: options.rev + }, + 'error sending file to third party data store queued up for processing' + ) + return callback(err) + } + logger.log( + { + project_id: options.project_id, + user_id, + path: options.path, + uri: options.uri, + rev: options.rev + }, + 'sending file to third party data store queued up for processing' + ) + return callback(err) + } + ) + }) + }, + + addFile(options, callback) { + if (callback == null) { + callback = function(err) {} + } + metrics.inc('tpds.add-file') + options.streamOrigin = + (settings.apis.filestore.linode_url || settings.apis.filestore.url) + + path.join(`/project/${options.project_id}/file/`, `${options.file_id}`) + return this._addEntity(options, callback) + }, + + addDoc(options, callback) { + if (callback == null) { + callback = function(err) {} + } + metrics.inc('tpds.add-doc') + options.streamOrigin = + (settings.apis.docstore.linode_url || settings.apis.docstore.pubUrl) + + path.join(`/project/${options.project_id}/doc/`, `${options.doc_id}/raw`) + return this._addEntity(options, callback) + }, + moveEntity(options, callback) { + let endPath, startPath + if (callback == null) { + callback = function(err) {} + } + metrics.inc('tpds.move-entity') + if (options.newProjectName != null) { + startPath = path.join(`/${options.project_name}/`) + endPath = path.join(`/${options.newProjectName}/`) + } else { + startPath = mergeProjectNameAndPath( + options.project_name, + options.startPath + ) + endPath = mergeProjectNameAndPath(options.project_name, options.endPath) + } + return getProjectsUsersIds(options.project_id, function( + err, + user_id, + allUserIds + ) { + logger.log( + { + project_id: options.project_id, + user_id, + startPath, + endPath, + uri: options.uri + }, + 'moving entity in third party data store' + ) + const moveOptions = { + method: 'put', + title: 'moveEntity', + uri: `${tpdsUrl}/user/${user_id}/entity`, + headers: { + sl_project_id: options.project_id, + sl_entity_rev: options.rev, + sl_all_user_ids: JSON.stringify(allUserIds) + }, + json: { + user_id, + endPath, + startPath + } + } + return TpdsUpdateSender._enqueue( + options.project_id, + 'standardHttpRequest', + moveOptions, + callback + ) + }) + }, + + deleteEntity(options, callback) { + if (callback == null) { + callback = function(err) {} + } + metrics.inc('tpds.delete-entity') + return getProjectsUsersIds(options.project_id, function( + err, + user_id, + allUserIds + ) { + logger.log( + { + project_id: options.project_id, + user_id, + path: options.path, + uri: options.uri + }, + 'deleting entity in third party data store' + ) + const deleteOptions = { + method: 'DELETE', + headers: { + sl_project_id: options.project_id, + sl_all_user_ids: JSON.stringify(allUserIds) + }, + uri: `${tpdsUrl}${buildPath( + user_id, + options.project_name, + options.path + )}`, + title: 'deleteEntity', + sl_all_user_ids: JSON.stringify(allUserIds) + } + return TpdsUpdateSender._enqueue( + options.project_id, + 'standardHttpRequest', + deleteOptions, + callback + ) + }) + }, + + pollDropboxForUser(user_id, callback) { + if (callback == null) { + callback = function(err) {} + } + metrics.inc('tpds.poll-dropbox') + logger.log({ user_id }, 'polling dropbox for user') + const options = { + method: 'POST', + uri: `${tpdsUrl}/user/poll`, + json: { + user_ids: [user_id] + } + } + return TpdsUpdateSender._enqueue( + `poll-dropbox:${user_id}`, + 'standardHttpRequest', + options, + callback + ) + } +} + +var getProjectsUsersIds = function(project_id, callback) { + if (callback == null) { + callback = function(err, owner_id, allUserIds) {} + } + return ProjectGetter.getProject( + project_id, + { _id: true, owner_ref: true }, + function(err, project) { + if (err != null) { + return callback(err) + } + return CollaboratorsHandler.getInvitedMemberIds(project_id, function( + err, + member_ids + ) { + if (err != null) { + return callback(err) + } + return callback( + err, + project != null ? project.owner_ref : undefined, + member_ids + ) + }) + } + ) +} + +var mergeProjectNameAndPath = function(project_name, path) { + if (path.indexOf('/') === 0) { + path = path.substring(1) + } + const fullPath = `/${project_name}/${path}` + return fullPath +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/UpdateMerger.js b/services/web/app/src/Features/ThirdPartyDataStore/UpdateMerger.js new file mode 100644 index 0000000000..dae021d7c0 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/UpdateMerger.js @@ -0,0 +1,203 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UpdateMerger +const _ = require('underscore') +const fs = require('fs') +const logger = require('logger-sharelatex') +const EditorController = require('../Editor/EditorController') +const FileTypeManager = require('../Uploads/FileTypeManager') +const FileWriter = require('../../infrastructure/FileWriter') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') + +module.exports = UpdateMerger = { + mergeUpdate(user_id, project_id, path, updateRequest, source, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log({ project_id, path }, 'merging update from tpds') + return FileWriter.writeStreamToDisk(project_id, updateRequest, function( + err, + fsPath + ) { + if (err != null) { + return callback(err) + } + return UpdateMerger._mergeUpdate( + user_id, + project_id, + path, + fsPath, + source, + mergeErr => + fs.unlink(fsPath, function(deleteErr) { + if (deleteErr != null) { + logger.err({ project_id, fsPath }, 'error deleting file') + } + return callback(mergeErr) + }) + ) + }) + }, + + _determineFileType(project_id, path, fsPath, callback) { + if (callback == null) { + callback = function(err, fileType) {} + } + return ProjectEntityHandler.getAllEntities(project_id, function( + err, + docs, + files + ) { + if (err != null) { + return callback(err) + } + if (_.some(files, f => f.path === path)) { + return callback(null, 'existing-file') + } + if (_.some(docs, d => d.path === path)) { + return callback(null, 'existing-doc') + } + // existing file not found in project, so check the file type to determine if doc + return FileTypeManager.getType(path, fsPath, function(err, isBinary) { + if (err != null) { + return callback(err) + } + if (isBinary) { + return callback(null, 'new-file') // extension was not text + } else { + return callback(null, 'new-doc') + } + }) + }) + }, + + _mergeUpdate(user_id, project_id, path, fsPath, source, callback) { + if (callback == null) { + callback = function(error) {} + } + return UpdateMerger._determineFileType(project_id, path, fsPath, function( + err, + fileType + ) { + if (err != null) { + return callback(err) + } + if (['existing-file', 'new-file'].includes(fileType)) { + return UpdateMerger.p.processFile( + project_id, + fsPath, + path, + source, + user_id, + callback + ) + } else if (['existing-doc', 'new-doc'].includes(fileType)) { + return UpdateMerger.p.processDoc( + project_id, + user_id, + fsPath, + path, + source, + callback + ) + } else { + return callback(new Error('unrecognized file')) + } + }) + }, + + deleteUpdate(user_id, project_id, path, source, callback) { + if (callback == null) { + callback = function() {} + } + return EditorController.deleteEntityWithPath( + project_id, + path, + source, + user_id, + function() { + logger.log( + { project_id, path }, + 'finished processing update to delete entity from tpds' + ) + return callback() + } + ) + }, + + p: { + processDoc(project_id, user_id, fsPath, path, source, callback) { + return UpdateMerger.p.readFileIntoTextArray(fsPath, function( + err, + docLines + ) { + if (err != null) { + logger.err( + { project_id }, + 'error reading file into text array for process doc update' + ) + return callback(err) + } + logger.log({ docLines }, 'processing doc update from tpds') + return EditorController.upsertDocWithPath( + project_id, + path, + docLines, + source, + user_id, + function(err) { + logger.log( + { project_id }, + 'completed processing file update from tpds' + ) + return callback(err) + } + ) + }) + }, + + processFile(project_id, fsPath, path, source, user_id, callback) { + logger.log({ project_id }, 'processing file update from tpds') + return EditorController.upsertFileWithPath( + project_id, + path, + fsPath, + null, + source, + user_id, + function(err) { + logger.log( + { project_id }, + 'completed processing file update from tpds' + ) + return callback(err) + } + ) + }, + + readFileIntoTextArray(path, callback) { + return fs.readFile(path, 'utf8', function(error, content) { + if (content == null) { + content = '' + } + if (error != null) { + logger.err({ path }, 'error reading file into text array') + return callback(error) + } + const lines = content.split(/\r\n|\n|\r/) + return callback(error, lines) + }) + } + } +} diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.js b/services/web/app/src/Features/TokenAccess/TokenAccessController.js new file mode 100644 index 0000000000..626e0026f4 --- /dev/null +++ b/services/web/app/src/Features/TokenAccess/TokenAccessController.js @@ -0,0 +1,316 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TokenAccessController +const ProjectController = require('../Project/ProjectController') +const AuthenticationController = require('../Authentication/AuthenticationController') +const TokenAccessHandler = require('./TokenAccessHandler') +const Features = require('../../infrastructure/Features') +const Errors = require('../Errors/Errors') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') + +module.exports = TokenAccessController = { + _loadEditor(projectId, req, res, next) { + req.params.Project_id = projectId.toString() + return ProjectController.loadEditor(req, res, next) + }, + + _tryHigherAccess(token, userId, req, res, next) { + return TokenAccessHandler.findProjectWithHigherAccess( + token, + userId, + function(err, project) { + if (err != null) { + logger.err( + { err, token, userId }, + '[TokenAccess] error finding project with higher access' + ) + return next(err) + } + if (project == null) { + logger.log( + { token, userId }, + '[TokenAccess] no project with higher access found for this user and token' + ) + return next(new Errors.NotFoundError()) + } + logger.log( + { token, userId, projectId: project._id }, + '[TokenAccess] user has higher access to project, redirecting' + ) + return res.redirect(302, `/project/${project._id}`) + } + ) + }, + + readAndWriteToken(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const token = req.params['read_and_write_token'] + logger.log( + { userId, token }, + '[TokenAccess] requesting read-and-write token access' + ) + return TokenAccessHandler.findProjectWithReadAndWriteToken(token, function( + err, + project, + projectExists + ) { + if (err != null) { + logger.err( + { err, token, userId }, + '[TokenAccess] error getting project by readAndWrite token' + ) + return next(err) + } + if (!projectExists && settings.overleaf) { + logger.log( + { token, userId }, + '[TokenAccess] no project found for this token' + ) + return TokenAccessController._handleV1Project( + token, + userId, + `/${token}`, + res, + next + ) + } else if (project == null) { + logger.log( + { token, userId }, + '[TokenAccess] no token-based project found for readAndWrite token' + ) + if (userId == null) { + logger.log( + { token }, + '[TokenAccess] No project found with read-write token, anonymous user, deny' + ) + return next(new Errors.NotFoundError()) + } + return TokenAccessController._tryHigherAccess( + token, + userId, + req, + res, + next + ) + } else { + if (userId == null) { + if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) { + logger.log( + { token, projectId: project._id }, + '[TokenAccess] allow anonymous read-and-write token access' + ) + TokenAccessHandler.grantSessionTokenAccess(req, project._id, token) + req._anonymousAccessToken = token + return TokenAccessController._loadEditor( + project._id, + req, + res, + next + ) + } else { + logger.log( + { token, projectId: project._id }, + '[TokenAccess] deny anonymous read-and-write token access' + ) + AuthenticationController.setRedirectInSession(req) + return res.redirect('/restricted') + } + } + if (project.owner_ref.toString() === userId) { + logger.log( + { userId, projectId: project._id }, + '[TokenAccess] user is already project owner' + ) + return TokenAccessController._loadEditor(project._id, req, res, next) + } + logger.log( + { userId, projectId: project._id }, + '[TokenAccess] adding user to project with readAndWrite token' + ) + return TokenAccessHandler.addReadAndWriteUserToProject( + userId, + project._id, + function(err) { + if (err != null) { + logger.err( + { err, token, userId, projectId: project._id }, + '[TokenAccess] error adding user to project with readAndWrite token' + ) + return next(err) + } + return TokenAccessController._loadEditor( + project._id, + req, + res, + next + ) + } + ) + } + }) + }, + + readOnlyToken(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const token = req.params['read_only_token'] + logger.log( + { userId, token }, + '[TokenAccess] requesting read-only token access' + ) + return TokenAccessHandler.getV1DocPublishedInfo(token, function( + err, + doc_published_info + ) { + if (err != null) { + return next(err) + } + if (doc_published_info.allow === false) { + return res.redirect(doc_published_info.published_path) + } + + return TokenAccessHandler.findProjectWithReadOnlyToken(token, function( + err, + project, + projectExists + ) { + if (err != null) { + logger.err( + { err, token, userId }, + '[TokenAccess] error getting project by readOnly token' + ) + return next(err) + } + if (!projectExists && settings.overleaf) { + logger.log( + { token, userId }, + '[TokenAccess] no project found for this token' + ) + return TokenAccessController._handleV1Project( + token, + userId, + `/read/${token}`, + res, + next + ) + } else if (project == null) { + logger.log( + { token, userId }, + '[TokenAccess] no project found for readOnly token' + ) + if (userId == null) { + logger.log( + { token }, + '[TokenAccess] No project found with readOnly token, anonymous user, deny' + ) + return next(new Errors.NotFoundError()) + } + return TokenAccessController._tryHigherAccess( + token, + userId, + req, + res, + next + ) + } else { + if (userId == null) { + logger.log( + { userId, projectId: project._id }, + '[TokenAccess] adding anonymous user to project with readOnly token' + ) + TokenAccessHandler.grantSessionTokenAccess(req, project._id, token) + req._anonymousAccessToken = token + return TokenAccessController._loadEditor( + project._id, + req, + res, + next + ) + } else { + if (project.owner_ref.toString() === userId) { + logger.log( + { userId, projectId: project._id }, + '[TokenAccess] user is already project owner' + ) + return TokenAccessController._loadEditor( + project._id, + req, + res, + next + ) + } + logger.log( + { userId, projectId: project._id }, + '[TokenAccess] adding user to project with readOnly token' + ) + return TokenAccessHandler.addReadOnlyUserToProject( + userId, + project._id, + function(err) { + if (err != null) { + logger.err( + { err, token, userId, projectId: project._id }, + '[TokenAccess] error adding user to project with readAndWrite token' + ) + return next(err) + } + return TokenAccessController._loadEditor( + project._id, + req, + res, + next + ) + } + ) + } + } + }) + }) + }, + + _handleV1Project(token, userId, redirectPath, res, next) { + if (userId == null) { + if (Features.hasFeature('force-import-to-v2')) { + return res.render('project/v2-import', { loginRedirect: redirectPath }) + } else { + return res.redirect(302, `/sign_in_to_v1?return_to=${redirectPath}`) + } + } else { + return TokenAccessHandler.getV1DocInfo(token, userId, function( + err, + doc_info + ) { + if (err != null) { + return next(err) + } + if (!doc_info.exists) { + return next(new Errors.NotFoundError()) + } + if (doc_info.exported) { + return next(new Errors.NotFoundError()) + } + if (Features.hasFeature('force-import-to-v2')) { + return res.render('project/v2-import', { + projectId: token, + hasOwner: doc_info.has_owner, + name: doc_info.name || 'Untitled', + hasAssignment: doc_info.has_assignment, + brandInfo: doc_info.brand_info + }) + } else { + return res.redirect(302, `/sign_in_to_v1?return_to=${redirectPath}`) + } + }) + } + } +} diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js new file mode 100644 index 0000000000..d00868611a --- /dev/null +++ b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js @@ -0,0 +1,366 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TokenAccessHandler +const { Project } = require('../../models/Project') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const UserGetter = require('../User/UserGetter') +const { ObjectId } = require('mongojs') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const V1Api = require('../V1/V1Api') +const crypto = require('crypto') + +module.exports = TokenAccessHandler = { + ANONYMOUS_READ_AND_WRITE_ENABLED: + Settings.allowAnonymousReadAndWriteSharing === true, + + _extractNumericPrefix(token) { + return token.match(/^(\d+)\w+/) + }, + + _getProjectByReadOnlyToken(token, callback) { + if (callback == null) { + callback = function(err, project) {} + } + return Project.findOne( + { + 'tokens.readOnly': token + }, + { _id: 1, tokens: 1, publicAccesLevel: 1, owner_ref: 1 }, + callback + ) + }, + + _getProjectByEitherToken(token, callback) { + if (callback == null) { + callback = function(err, project) {} + } + return TokenAccessHandler._getProjectByReadOnlyToken(token, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if (project != null) { + return callback(null, project) + } + return TokenAccessHandler._getProjectByReadAndWriteToken(token, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + return callback(null, project) + }) + }) + }, + + _getProjectByReadAndWriteToken(token, callback) { + if (callback == null) { + callback = function(err, project) {} + } + const numericPrefixMatch = TokenAccessHandler._extractNumericPrefix(token) + if (!numericPrefixMatch) { + return callback(null, null) + } + const numerics = numericPrefixMatch[1] + return Project.findOne( + { + 'tokens.readAndWritePrefix': numerics + }, + { _id: 1, tokens: 1, publicAccesLevel: 1, owner_ref: 1 }, + function(err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(null, null) + } + try { + if ( + !crypto.timingSafeEqual( + new Buffer(token), + new Buffer(project.tokens.readAndWrite) + ) + ) { + logger.err( + { token }, + 'read-and-write token match on numeric section, but not on full token' + ) + return callback(null, null) + } else { + return callback(null, project) + } + } catch (error) { + err = error + logger.err({ token, cryptoErr: err }, 'error comparing tokens') + return callback(null, null) + } + } + ) + }, + + findProjectWithReadOnlyToken(token, callback) { + if (callback == null) { + callback = function(err, project, projectExists) {} + } + return TokenAccessHandler._getProjectByReadOnlyToken(token, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(null, null, false) // Project doesn't exist, so we handle differently + } + if (project.publicAccesLevel !== PublicAccessLevels.TOKEN_BASED) { + return callback(null, null, true) // Project does exist, but it isn't token based + } + return callback(null, project, true) + }) + }, + + findProjectWithReadAndWriteToken(token, callback) { + if (callback == null) { + callback = function(err, project, projectExists) {} + } + return TokenAccessHandler._getProjectByReadAndWriteToken(token, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(null, null, false) // Project doesn't exist, so we handle differently + } + if (project.publicAccesLevel !== PublicAccessLevels.TOKEN_BASED) { + return callback(null, null, true) // Project does exist, but it isn't token based + } + return callback(null, project, true) + }) + }, + + _userIsMember(userId, projectId, callback) { + if (callback == null) { + callback = function(err, isMember) {} + } + return CollaboratorsHandler.isUserInvitedMemberOfProject( + userId, + projectId, + callback + ) + }, + + findProjectWithHigherAccess(token, userId, callback) { + if (callback == null) { + callback = function(err, project) {} + } + return TokenAccessHandler._getProjectByEitherToken(token, function( + err, + project + ) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(null, null) + } + const projectId = project._id + return TokenAccessHandler._userIsMember(userId, projectId, function( + err, + isMember + ) { + if (err != null) { + return callback(err) + } + return callback(null, isMember === true ? project : null) + }) + }) + }, + + addReadOnlyUserToProject(userId, projectId, callback) { + if (callback == null) { + callback = function(err) {} + } + userId = ObjectId(userId.toString()) + projectId = ObjectId(projectId.toString()) + return Project.update( + { + _id: projectId + }, + { + $addToSet: { tokenAccessReadOnly_refs: userId } + }, + callback + ) + }, + + addReadAndWriteUserToProject(userId, projectId, callback) { + if (callback == null) { + callback = function(err) {} + } + userId = ObjectId(userId.toString()) + projectId = ObjectId(projectId.toString()) + return Project.update( + { + _id: projectId + }, + { + $addToSet: { tokenAccessReadAndWrite_refs: userId } + }, + callback + ) + }, + + grantSessionTokenAccess(req, projectId, token) { + if (req.session != null) { + if (req.session.anonTokenAccess == null) { + req.session.anonTokenAccess = {} + } + return (req.session.anonTokenAccess[ + projectId.toString() + ] = token.toString()) + } + }, + + getRequestToken(req, projectId) { + const token = + __guard__( + __guard__( + req != null ? req.session : undefined, + x1 => x1.anonTokenAccess + ), + x => x[projectId.toString()] + ) || + (req != null ? req.headers['x-sl-anonymous-access-token'] : undefined) + return token + }, + + isValidToken(projectId, token, callback) { + if (callback == null) { + callback = function(err, isValidReadAndWrite, isValidReadOnly) {} + } + if (!token) { + return callback(null, false, false) + } + const _validate = project => + project != null && + project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED && + project._id.toString() === projectId.toString() + return TokenAccessHandler.findProjectWithReadAndWriteToken(token, function( + err, + readAndWriteProject + ) { + if (err != null) { + return callback(err) + } + const isValidReadAndWrite = _validate(readAndWriteProject) + return TokenAccessHandler.findProjectWithReadOnlyToken(token, function( + err, + readOnlyProject + ) { + if (err != null) { + return callback(err) + } + const isValidReadOnly = _validate(readOnlyProject) + return callback(null, isValidReadAndWrite, isValidReadOnly) + }) + }) + }, + + protectTokens(project, privilegeLevel) { + if (project != null && project.tokens != null) { + if (privilegeLevel === PrivilegeLevels.OWNER) { + return + } + if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) { + project.tokens.readAndWrite = '' + project.tokens.readAndWritePrefix = '' + } + if (privilegeLevel !== PrivilegeLevels.READ_ONLY) { + return (project.tokens.readOnly = '') + } + } + }, + + getV1DocPublishedInfo(token, callback) { + // default to allowing access + if (callback == null) { + callback = function(err, publishedInfo) {} + } + if ((Settings.apis != null ? Settings.apis.v1 : undefined) == null) { + return callback(null, { + allow: true + }) + } + + return V1Api.request( + { url: `/api/v1/sharelatex/docs/${token}/is_published` }, + function(err, response, body) { + if (err != null) { + return callback(err) + } + return callback(null, body) + } + ) + }, + + getV1DocInfo(token, v2UserId, callback) { + // default to not exported + if (callback == null) { + callback = function(err, info) {} + } + if ((Settings.apis != null ? Settings.apis.v1 : undefined) == null) { + return callback(null, { + exists: true, + exported: false + }) + } + + return UserGetter.getUser(v2UserId, { overleaf: 1 }, function(err, user) { + if (err != null) { + return callback(err) + } + const v1UserId = user.overleaf != null ? user.overleaf.id : undefined + return V1Api.request( + { url: `/api/v1/sharelatex/users/${v1UserId}/docs/${token}/info` }, + function(err, response, body) { + if (err != null) { + return callback(err) + } + return callback(null, body) + } + ) + }) + } +} + +module.exports.READ_AND_WRITE_TOKEN_REGEX = /^(\d+)(\w+)$/ +module.exports.READ_AND_WRITE_URL_REGEX = /^\/(\d+)(\w+)$/ +module.exports.READ_ONLY_TOKEN_REGEX = /^([a-z]{12})$/ +module.exports.READ_ONLY_URL_REGEX = /^\/read\/([a-z]{12})$/ + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Uploads/ArchiveManager.js b/services/web/app/src/Features/Uploads/ArchiveManager.js new file mode 100644 index 0000000000..b32849185e --- /dev/null +++ b/services/web/app/src/Features/Uploads/ArchiveManager.js @@ -0,0 +1,244 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ArchiveManager +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const fs = require('fs') +const Path = require('path') +const fse = require('fs-extra') +const yauzl = require('yauzl') +const Settings = require('settings-sharelatex') +const Errors = require('../Errors/Errors') +const _ = require('underscore') + +const ONE_MEG = 1024 * 1024 + +module.exports = ArchiveManager = { + _isZipTooLarge(source, callback) { + if (callback == null) { + callback = function(err, isTooLarge) {} + } + callback = _.once(callback) + + let totalSizeInBytes = null + return yauzl.open(source, { lazyEntries: true }, function(err, zipfile) { + if (err != null) { + return callback(new Errors.InvalidError('invalid_zip_file')) + } + + if ( + Settings.maxEntitiesPerProject != null && + zipfile.entryCount > Settings.maxEntitiesPerProject + ) { + return callback(null, true) // too many files in zip file + } + + zipfile.on('error', callback) + + // read all the entries + zipfile.readEntry() + zipfile.on('entry', function(entry) { + totalSizeInBytes += entry.uncompressedSize + return zipfile.readEntry() + }) // get the next entry + + // no more entries to read + return zipfile.on('end', function() { + if (totalSizeInBytes == null || isNaN(totalSizeInBytes)) { + logger.err({ source, totalSizeInBytes }, 'error getting bytes of zip') + return callback(new Error('error getting bytes of zip')) + } + const isTooLarge = totalSizeInBytes > ONE_MEG * 300 + return callback(null, isTooLarge) + }) + }) + }, + + _checkFilePath(entry, destination, callback) { + // transform backslashes to forwardslashes to accommodate badly-behaved zip archives + if (callback == null) { + callback = function(err, destFile) {} + } + const transformedFilename = entry.fileName.replace(/\\/g, '/') + // check if the entry is a directory + const endsWithSlash = /\/$/ + if (endsWithSlash.test(transformedFilename)) { + return callback() // don't give a destfile for directory + } + // check that the file does not use a relative path + for (let dir of Array.from(transformedFilename.split('/'))) { + if (dir === '..') { + return callback(new Error('relative path')) + } + } + // check that the destination file path is normalized + const dest = `${destination}/${transformedFilename}` + if (dest !== Path.normalize(dest)) { + return callback(new Error('unnormalized path')) + } else { + return callback(null, dest) + } + }, + + _writeFileEntry(zipfile, entry, destFile, callback) { + if (callback == null) { + callback = function(err) {} + } + callback = _.once(callback) + + return zipfile.openReadStream(entry, function(err, readStream) { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + readStream.on('end', callback) + + const errorHandler = function(err) { + // clean up before calling callback + readStream.unpipe() + readStream.destroy() + return callback(err) + } + + return fse.ensureDir(Path.dirname(destFile), function(err) { + if (err != null) { + return errorHandler(err) + } + const writeStream = fs.createWriteStream(destFile) + writeStream.on('error', errorHandler) + return readStream.pipe(writeStream) + }) + }) + }, + + _extractZipFiles(source, destination, callback) { + if (callback == null) { + callback = function(err) {} + } + callback = _.once(callback) + + return yauzl.open(source, { lazyEntries: true }, function(err, zipfile) { + if (err != null) { + return callback(err) + } + zipfile.on('error', callback) + // read all the entries + zipfile.readEntry() + zipfile.on('entry', function(entry) { + logger.log( + { source, fileName: entry.fileName }, + 'processing zip file entry' + ) + return ArchiveManager._checkFilePath(entry, destination, function( + err, + destFile + ) { + if (err != null) { + logger.warn({ err, source, destination }, 'skipping bad file path') + zipfile.readEntry() // bad path, just skip to the next file + return + } + if (destFile != null) { + // only write files + return ArchiveManager._writeFileEntry( + zipfile, + entry, + destFile, + function(err) { + if (err != null) { + logger.error( + { err, source, destFile }, + 'error unzipping file entry' + ) + zipfile.close() // bail out, stop reading file entries + return callback(err) + } else { + return zipfile.readEntry() + } + } + ) // continue to the next file + } else { + // if it's a directory, continue + return zipfile.readEntry() + } + }) + }) + // no more entries to read + return zipfile.on('end', callback) + }) + }, + + extractZipArchive(source, destination, _callback) { + if (_callback == null) { + _callback = function(err) {} + } + const callback = function(...args) { + _callback(...Array.from(args || [])) + return (_callback = function() {}) + } + + return ArchiveManager._isZipTooLarge(source, function(err, isTooLarge) { + if (err != null) { + logger.err({ err }, 'error checking size of zip file') + return callback(err) + } + + if (isTooLarge) { + return callback(new Errors.InvalidError('zip_contents_too_large')) + } + + const timer = new metrics.Timer('unzipDirectory') + logger.log({ source, destination }, 'unzipping file') + + return ArchiveManager._extractZipFiles(source, destination, function( + err + ) { + timer.done() + if (err != null) { + logger.error({ err, source, destination }, 'unzip failed') + return callback(err) + } else { + return callback() + } + }) + }) + }, + + findTopLevelDirectory(directory, callback) { + if (callback == null) { + callback = function(error, topLevelDir) {} + } + return fs.readdir(directory, function(error, files) { + if (error != null) { + return callback(error) + } + if (files.length === 1) { + const childPath = Path.join(directory, files[0]) + return fs.stat(childPath, function(error, stat) { + if (error != null) { + return callback(error) + } + if (stat.isDirectory()) { + return callback(null, childPath) + } else { + return callback(null, directory) + } + }) + } else { + return callback(null, directory) + } + }) + } +} diff --git a/services/web/app/src/Features/Uploads/FileSystemImportManager.js b/services/web/app/src/Features/Uploads/FileSystemImportManager.js new file mode 100644 index 0000000000..b8b658662e --- /dev/null +++ b/services/web/app/src/Features/Uploads/FileSystemImportManager.js @@ -0,0 +1,311 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FileSystemImportManager +const async = require('async') +const fs = require('fs') +const _ = require('underscore') +const FileTypeManager = require('./FileTypeManager') +const EditorController = require('../Editor/EditorController') +const logger = require('logger-sharelatex') + +module.exports = FileSystemImportManager = { + addDoc( + user_id, + project_id, + folder_id, + name, + path, + charset, + replace, + callback + ) { + if (callback == null) { + callback = function(error, doc) {} + } + return FileSystemImportManager._isSafeOnFileSystem(path, function( + err, + isSafe + ) { + if (!isSafe) { + logger.log( + { user_id, project_id, folder_id, name, path }, + 'add doc is from symlink, stopping process' + ) + return callback('path is symlink') + } + return fs.readFile(path, charset, function(error, content) { + if (error != null) { + return callback(error) + } + content = content.replace(/\r\n?/g, '\n') // convert Windows line endings to unix. very old macs also created \r-separated lines + const lines = content.split('\n') + if (replace) { + return EditorController.upsertDoc( + project_id, + folder_id, + name, + lines, + 'upload', + user_id, + callback + ) + } else { + return EditorController.addDoc( + project_id, + folder_id, + name, + lines, + 'upload', + user_id, + callback + ) + } + }) + }) + }, + + addFile(user_id, project_id, folder_id, name, path, replace, callback) { + if (callback == null) { + callback = function(error, file) {} + } + return FileSystemImportManager._isSafeOnFileSystem(path, function( + err, + isSafe + ) { + if (!isSafe) { + logger.log( + { user_id, project_id, folder_id, name, path }, + 'add file is from symlink, stopping insert' + ) + return callback('path is symlink') + } + + if (replace) { + return EditorController.upsertFile( + project_id, + folder_id, + name, + path, + null, + 'upload', + user_id, + callback + ) + } else { + return EditorController.addFile( + project_id, + folder_id, + name, + path, + null, + 'upload', + user_id, + callback + ) + } + }) + }, + + addFolder(user_id, project_id, folder_id, name, path, replace, callback) { + if (callback == null) { + callback = function(error) {} + } + return FileSystemImportManager._isSafeOnFileSystem(path, function( + err, + isSafe + ) { + if (!isSafe) { + logger.log( + { user_id, project_id, folder_id, path }, + 'add folder is from symlink, stopping insert' + ) + return callback('path is symlink') + } + return EditorController.addFolder( + project_id, + folder_id, + name, + 'upload', + (error, new_folder) => { + if (error != null) { + return callback(error) + } + return FileSystemImportManager.addFolderContents( + user_id, + project_id, + new_folder._id, + path, + replace, + function(error) { + if (error != null) { + return callback(error) + } + return callback(null, new_folder) + } + ) + } + ) + }) + }, + + addFolderContents( + user_id, + project_id, + parent_folder_id, + folderPath, + replace, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + return FileSystemImportManager._isSafeOnFileSystem(folderPath, function( + err, + isSafe + ) { + if (!isSafe) { + logger.log( + { user_id, project_id, parent_folder_id, folderPath }, + 'add folder contents is from symlink, stopping insert' + ) + return callback('path is symlink') + } + return fs.readdir(folderPath, (error, entries) => { + if (entries == null) { + entries = [] + } + if (error != null) { + return callback(error) + } + return async.eachSeries( + entries, + (entry, callback) => { + return FileTypeManager.shouldIgnore(entry, (error, ignore) => { + if (error != null) { + return callback(error) + } + if (!ignore) { + return FileSystemImportManager.addEntity( + user_id, + project_id, + parent_folder_id, + entry, + `${folderPath}/${entry}`, + replace, + callback + ) + } else { + return callback() + } + }) + }, + callback + ) + }) + }) + }, + + addEntity(user_id, project_id, folder_id, name, path, replace, callback) { + if (callback == null) { + callback = function(error, entity) {} + } + return FileSystemImportManager._isSafeOnFileSystem(path, function( + err, + isSafe + ) { + if (!isSafe) { + logger.log( + { user_id, project_id, folder_id, path }, + 'add entry is from symlink, stopping insert' + ) + return callback('path is symlink') + } + + return FileTypeManager.isDirectory(path, (error, isDirectory) => { + if (error != null) { + return callback(error) + } + if (isDirectory) { + return FileSystemImportManager.addFolder( + user_id, + project_id, + folder_id, + name, + path, + replace, + callback + ) + } else { + return FileTypeManager.getType( + name, + path, + (error, isBinary, charset) => { + if (error != null) { + return callback(error) + } + if (isBinary) { + return FileSystemImportManager.addFile( + user_id, + project_id, + folder_id, + name, + path, + replace, + function(err, entity) { + if (entity != null) { + entity.type = 'file' + } + return callback(err, entity) + } + ) + } else { + return FileSystemImportManager.addDoc( + user_id, + project_id, + folder_id, + name, + path, + charset, + replace, + function(err, entity) { + if (entity != null) { + entity.type = 'doc' + } + return callback(err, entity) + } + ) + } + } + ) + } + }) + }) + }, + + _isSafeOnFileSystem(path, callback) { + if (callback == null) { + callback = function(err, isSafe) {} + } + return fs.lstat(path, function(err, stat) { + if (err != null) { + logger.err({ err }, 'error with path symlink check') + return callback(err) + } + const isSafe = stat.isFile() || stat.isDirectory() + return callback(err, isSafe) + }) + } +} diff --git a/services/web/app/src/Features/Uploads/FileTypeManager.js b/services/web/app/src/Features/Uploads/FileTypeManager.js new file mode 100644 index 0000000000..41810ceecc --- /dev/null +++ b/services/web/app/src/Features/Uploads/FileTypeManager.js @@ -0,0 +1,140 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FileTypeManager +const fs = require('fs') +const Path = require('path') +const isUtf8 = require('is-utf8') + +module.exports = FileTypeManager = { + TEXT_EXTENSIONS: [ + 'tex', + 'latex', + 'sty', + 'cls', + 'bst', + 'bib', + 'bibtex', + 'txt', + 'tikz', + 'rtex', + 'md', + 'asy', + 'latexmkrc', + 'lbx', + 'bbx', + 'cbx', + 'm' + ], + + IGNORE_EXTENSIONS: [ + 'dvi', + 'aux', + 'log', + 'toc', + 'out', + 'pdfsync', + // Index and glossary files + 'nlo', + 'ind', + 'glo', + 'gls', + 'glg', + // Bibtex + 'bbl', + 'blg', + // Misc/bad + 'doc', + 'docx', + 'gz' + ], + + IGNORE_FILENAMES: ['__MACOSX', '.git', '.gitignore'], + + MAX_TEXT_FILE_SIZE: 1 * 1024 * 1024, // 1 MB + + isDirectory(path, callback) { + if (callback == null) { + callback = function(error, result) {} + } + return fs.stat(path, function(error, stats) { + if (error != null) { + return callback(error) + } + return callback(null, stats != null ? stats.isDirectory() : undefined) + }) + }, + + // returns charset as understood by fs.readFile, + getType(name, fsPath, callback) { + if (callback == null) { + callback = function(error, isBinary, charset) {} + } + const parts = name.split('.') + const extension = parts.slice(-1)[0].toLowerCase() + const isText = + (FileTypeManager.TEXT_EXTENSIONS.indexOf(extension) > -1 && + parts.length > 1) || + parts[0] === 'latexmkrc' + + if (!isText) { + return callback(null, true) + } + + return fs.stat(fsPath, function(error, stat) { + if (error != null) { + return callback(error) + } + if (stat.size > FileTypeManager.MAX_TEXT_FILE_SIZE) { + return callback(null, true) // Treat large text file as binary + } + + return fs.readFile(fsPath, function(err, bytes) { + if (err != null) { + return callback(err) + } + + if (isUtf8(bytes)) { + return callback(null, false, 'utf-8') + } + // check for little-endian unicode bom (nodejs does not support big-endian) + if (bytes[0] === 0xff && bytes[1] === 0xfe) { + return callback(null, false, 'utf-16le') + } + + return callback(null, false, 'latin1') + }) + }) + }, + + shouldIgnore(path, callback) { + if (callback == null) { + callback = function(error, result) {} + } + const name = Path.basename(path) + let extension = name.split('.').slice(-1)[0] + if (extension != null) { + extension = extension.toLowerCase() + } + let ignore = false + if (name[0] === '.' && extension !== 'latexmkrc') { + ignore = true + } + if (this.IGNORE_EXTENSIONS.indexOf(extension) !== -1) { + ignore = true + } + if (this.IGNORE_FILENAMES.indexOf(name) !== -1) { + ignore = true + } + return callback(null, ignore) + } +} diff --git a/services/web/app/src/Features/Uploads/ProjectUploadController.js b/services/web/app/src/Features/Uploads/ProjectUploadController.js new file mode 100644 index 0000000000..fbbfe38589 --- /dev/null +++ b/services/web/app/src/Features/Uploads/ProjectUploadController.js @@ -0,0 +1,153 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let err, ProjectUploadController +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const fs = require('fs') +const Path = require('path') +const FileSystemImportManager = require('./FileSystemImportManager') +const ProjectUploadManager = require('./ProjectUploadManager') +const AuthenticationController = require('../Authentication/AuthenticationController') +const Settings = require('settings-sharelatex') +const Errors = require('../Errors/Errors') +const multer = require('multer') + +let upload = null + +try { + upload = multer({ + dest: Settings.path.uploadFolder, + limits: { + fileSize: Settings.maxUploadSize + } + }) +} catch (error) { + err = error + if (err.message === 'EEXIST') { + logger.log( + { uploadFolder: Settings.path.uploadFolder }, + 'dir already exists, continuing' + ) + } else { + logger.err({ err }, 'caught error from multer in uploads router') + } +} + +module.exports = ProjectUploadController = { + uploadProject(req, res, next) { + const timer = new metrics.Timer('project-upload') + const user_id = AuthenticationController.getLoggedInUserId(req) + const { originalname, path } = req.file + const name = Path.basename(originalname, '.zip') + return ProjectUploadManager.createProjectFromZipArchive( + user_id, + name, + path, + function(error, project) { + fs.unlink(path, function() {}) + timer.done() + if (error != null) { + logger.error( + { err: error, file_path: path, file_name: name }, + 'error uploading project' + ) + if (error.name != null && error.name === 'InvalidError') { + return res.status(422).json({ + success: false, + error: req.i18n.translate(error.message) + }) + } else { + return res.status(500).json({ + success: false, + error: req.i18n.translate('upload_failed') + }) + } + } else { + logger.log( + { project: project._id, file_path: path, file_name: name }, + 'uploaded project' + ) + return res.send({ success: true, project_id: project._id }) + } + } + ) + }, + + uploadFile(req, res, next) { + const timer = new metrics.Timer('file-upload') + const name = req.file != null ? req.file.originalname : undefined + const path = req.file != null ? req.file.path : undefined + const project_id = req.params.Project_id + const { folder_id } = req.query + if (name == null || name.length === 0 || name.length > 150) { + logger.err({ project_id, name }, 'bad name when trying to upload file') + return res.send({ success: false }) + } + logger.log({ folder_id, project_id }, 'getting upload file request') + const user_id = AuthenticationController.getLoggedInUserId(req) + + return FileSystemImportManager.addEntity( + user_id, + project_id, + folder_id, + name, + path, + true, + function(error, entity) { + fs.unlink(path, function() {}) + timer.done() + if (error != null) { + logger.error( + { + err: error, + project_id, + file_path: path, + file_name: name, + folder_id + }, + 'error uploading file' + ) + return res.send({ success: false }) + } else { + logger.log( + { project_id, file_path: path, file_name: name, folder_id }, + 'uploaded file' + ) + return res.send({ + success: true, + entity_id: entity != null ? entity._id : undefined, + entity_type: entity != null ? entity.type : undefined + }) + } + } + ) + }, + + multerMiddleware(req, res, next) { + if (upload == null) { + return res + .status(500) + .json({ success: false, error: req.i18n.translate('upload_failed') }) + } + return upload.single('qqfile')(req, res, function(err) { + if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') { + return res + .status(422) + .json({ success: false, error: req.i18n.translate('file_too_large') }) + } + + return next(err) + }) + } +} diff --git a/services/web/app/src/Features/Uploads/ProjectUploadManager.js b/services/web/app/src/Features/Uploads/ProjectUploadManager.js new file mode 100644 index 0000000000..1e813283ef --- /dev/null +++ b/services/web/app/src/Features/Uploads/ProjectUploadManager.js @@ -0,0 +1,202 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectUploadHandler +const path = require('path') +const rimraf = require('rimraf') +const async = require('async') +const ArchiveManager = require('./ArchiveManager') +const FileSystemImportManager = require('./FileSystemImportManager') +const ProjectCreationHandler = require('../Project/ProjectCreationHandler') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const DocumentHelper = require('../Documents/DocumentHelper') + +module.exports = ProjectUploadHandler = { + createProjectFromZipArchive(owner_id, defaultName, zipPath, callback) { + if (callback == null) { + callback = function(error, project) {} + } + const destination = this._getDestinationDirectory(zipPath) + let docPath = null + let project = null + + return async.waterfall( + [ + cb => ArchiveManager.extractZipArchive(zipPath, destination, cb), + cb => + ProjectRootDocManager.findRootDocFileFromDirectory( + destination, + (error, _docPath, docContents) => cb(error, _docPath, docContents) + ), + function(_docPath, docContents, cb) { + docPath = _docPath + const proposedName = ProjectDetailsHandler.fixProjectName( + DocumentHelper.getTitleFromTexContent(docContents || '') || + defaultName + ) + return ProjectDetailsHandler.generateUniqueName( + owner_id, + proposedName, + (error, name) => cb(error, name) + ) + }, + (name, cb) => + ProjectCreationHandler.createBlankProject( + owner_id, + name, + (error, _project) => cb(error, _project) + ), + (_project, cb) => { + project = _project + return this._insertZipContentsIntoFolder( + owner_id, + project._id, + project.rootFolder[0]._id, + destination, + cb + ) + }, + function(cb) { + if (docPath != null) { + return ProjectRootDocManager.setRootDocFromName( + project._id, + docPath, + error => cb(error) + ) + } else { + return cb(null) + } + }, + cb => cb(null, project) + ], + callback + ) + }, + + createProjectFromZipArchiveWithName( + owner_id, + proposedName, + zipPath, + callback + ) { + if (callback == null) { + callback = function(error, project) {} + } + return ProjectDetailsHandler.generateUniqueName( + owner_id, + ProjectDetailsHandler.fixProjectName(proposedName), + (error, name) => { + if (error != null) { + return callback(error) + } + return ProjectCreationHandler.createBlankProject( + owner_id, + name, + (error, project) => { + if (error != null) { + return callback(error) + } + return this.insertZipArchiveIntoFolder( + owner_id, + project._id, + project.rootFolder[0]._id, + zipPath, + function(error) { + if (error != null) { + return callback(error) + } + return ProjectRootDocManager.setRootDocAutomatically( + project._id, + function(error) { + if (error != null) { + return callback(error) + } + return callback(error, project) + } + ) + } + ) + } + ) + } + ) + }, + + insertZipArchiveIntoFolder( + owner_id, + project_id, + folder_id, + zipPath, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + const destination = this._getDestinationDirectory(zipPath) + return ArchiveManager.extractZipArchive(zipPath, destination, error => { + if (error != null) { + return callback(error) + } + + return this._insertZipContentsIntoFolder( + owner_id, + project_id, + folder_id, + destination, + callback + ) + }) + }, + + _insertZipContentsIntoFolder( + owner_id, + project_id, + folder_id, + destination, + callback + ) { + if (callback == null) { + callback = function(error) {} + } + return ArchiveManager.findTopLevelDirectory(destination, function( + error, + topLevelDestination + ) { + if (error != null) { + return callback(error) + } + return FileSystemImportManager.addFolderContents( + owner_id, + project_id, + folder_id, + topLevelDestination, + false, + function(error) { + if (error != null) { + return callback(error) + } + return rimraf(destination, callback) + } + ) + }) + }, + + _getDestinationDirectory(source) { + return path.join( + path.dirname(source), + `${path.basename(source, '.zip')}-${Date.now()}` + ) + } +} diff --git a/services/web/app/src/Features/Uploads/UploadsRouter.js b/services/web/app/src/Features/Uploads/UploadsRouter.js new file mode 100644 index 0000000000..946825b6c7 --- /dev/null +++ b/services/web/app/src/Features/Uploads/UploadsRouter.js @@ -0,0 +1,45 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const AuthenticationController = require('../Authentication/AuthenticationController') +const ProjectUploadController = require('./ProjectUploadController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const Settings = require('settings-sharelatex') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.post( + '/project/new/upload', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'project-upload', + maxRequests: 20, + timeInterval: 60 + }), + ProjectUploadController.multerMiddleware, + ProjectUploadController.uploadProject + ) + + return webRouter.post( + '/Project/:Project_id/upload', + RateLimiterMiddleware.rateLimit({ + endpointName: 'file-upload', + params: ['Project_id'], + maxRequests: 200, + timeInterval: 60 * 30 + }), + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ProjectUploadController.multerMiddleware, + ProjectUploadController.uploadFile + ) + } +} diff --git a/services/web/app/src/Features/User/ThirdPartyIdentityManager.js b/services/web/app/src/Features/User/ThirdPartyIdentityManager.js new file mode 100644 index 0000000000..61bc42b4f9 --- /dev/null +++ b/services/web/app/src/Features/User/ThirdPartyIdentityManager.js @@ -0,0 +1,218 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ThirdPartyIdentityManager +const Errors = require('../Errors/Errors') +const { User } = require('../../models/User') +const { UserStub } = require('../../models/UserStub') +const UserUpdater = require('./UserUpdater') +const _ = require('lodash') + +module.exports = ThirdPartyIdentityManager = { + getUser(providerId, externalUserId, callback) { + if (providerId == null || externalUserId == null) { + return callback(new Error('invalid arguments')) + } + const query = ThirdPartyIdentityManager._getUserQuery( + providerId, + externalUserId + ) + return User.findOne(query, function(err, user) { + if (err != null) { + return callback(err) + } + if (!user) { + return callback(new Errors.ThirdPartyUserNotFoundError()) + } + return callback(null, user) + }) + }, + + login(providerId, externalUserId, externalData, callback) { + return ThirdPartyIdentityManager.getUser( + providerId, + externalUserId, + function(err, user) { + if (err != null) { + return callback(err) + } + if (!externalData) { + return callback(null, user) + } + const query = ThirdPartyIdentityManager._getUserQuery( + providerId, + externalUserId + ) + const update = ThirdPartyIdentityManager._thirdPartyIdentifierUpdate( + user, + providerId, + externalUserId, + externalData + ) + return User.findOneAndUpdate(query, update, { new: true }, callback) + } + ) + }, + + // attempt to login normally but check for user stub if user not found + loginUserStub(providerId, externalUserId, externalData, callback) { + return ThirdPartyIdentityManager.login( + providerId, + externalUserId, + externalData, + function(err, user) { + if (err == null) { + return callback(null, user) + } + if (err.name !== 'ThirdPartyUserNotFoundError') { + return callback(err) + } + const query = ThirdPartyIdentityManager._getUserQuery( + providerId, + externalUserId + ) + return UserStub.findOne(query, function(err, userStub) { + if (err != null) { + return callback(err) + } + if (!userStub) { + return callback(new Errors.ThirdPartyUserNotFoundError()) + } + if (!externalData) { + return callback(null, userStub) + } + const update = ThirdPartyIdentityManager._thirdPartyIdentifierUpdate( + userStub, + providerId, + externalUserId, + externalData + ) + return UserStub.findOneAndUpdate( + query, + update, + { new: true }, + callback + ) + }) + } + ) + }, + + _getUserQuery(providerId, externalUserId) { + externalUserId = externalUserId.toString() + providerId = providerId.toString() + const query = { + 'thirdPartyIdentifiers.externalUserId': externalUserId, + 'thirdPartyIdentifiers.providerId': providerId + } + return query + }, + + _thirdPartyIdentifierUpdate(user, providerId, externalUserId, externalData) { + providerId = providerId.toString() + // get third party identifier object from array + const thirdPartyIdentifier = user.thirdPartyIdentifiers.find( + tpi => + tpi.externalUserId === externalUserId && tpi.providerId === providerId + ) + // do recursive merge of new data over existing data + _.merge(thirdPartyIdentifier.externalData, externalData) + const update = { 'thirdPartyIdentifiers.$': thirdPartyIdentifier } + return update + }, + + // register: () -> + // this should be implemented once we move to having v2 as the master + // but for now we need to register with v1 then call link once that + // is complete + + link(user_id, providerId, externalUserId, externalData, callback, retry) { + const query = { + _id: user_id, + 'thirdPartyIdentifiers.providerId': { + $ne: providerId + } + } + const update = { + $push: { + thirdPartyIdentifiers: { + externalUserId, + externalData, + providerId + } + } + } + // add new tpi only if an entry for the provider does not exist + return UserUpdater.updateUser(query, update, function(err, res) { + if (err != null) { + return callback(err) + } + if (res.nModified === 1) { + return callback(null, res) + } + // if already retried then throw error + if (retry) { + return callback(new Error('update failed')) + } + // attempt to clear existing entry then retry + return ThirdPartyIdentityManager.unlink(user_id, providerId, function( + err + ) { + if (err != null) { + return callback(err) + } + return ThirdPartyIdentityManager.link( + user_id, + providerId, + externalUserId, + externalData, + callback, + true + ) + }) + }) + }, + + unlink(user_id, providerId, callback) { + const update = { + $pull: { + thirdPartyIdentifiers: { + providerId + } + } + } + return UserUpdater.updateUser(user_id, update, callback) + }, + + // attempt to unlink user but unlink user stub if not linked to user + unlinkUserStub(user_id, providerId, callback) { + return ThirdPartyIdentityManager.unlink(user_id, providerId, function( + err, + res + ) { + if (err != null) { + return callback(err) + } + if (res.nModified === 1) { + return callback(null, res) + } + const update = { + $pull: { + thirdPartyIdentifiers: { + providerId + } + } + } + return UserStub.update({ _id: user_id }, update, callback) + }) + } +} diff --git a/services/web/app/src/Features/User/UserController.js b/services/web/app/src/Features/User/UserController.js new file mode 100644 index 0000000000..f0df4b9868 --- /dev/null +++ b/services/web/app/src/Features/User/UserController.js @@ -0,0 +1,383 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserController +const UserHandler = require('./UserHandler') +const UserDeleter = require('./UserDeleter') +const UserGetter = require('./UserGetter') +const { User } = require('../../models/User') +const newsLetterManager = require('../Newsletter/NewsletterManager') +const UserRegistrationHandler = require('./UserRegistrationHandler') +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const Url = require('url') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const AuthenticationController = require('../Authentication/AuthenticationController') +const UserSessionsManager = require('./UserSessionsManager') +const UserUpdater = require('./UserUpdater') +const SudoModeHandler = require('../SudoMode/SudoModeHandler') +const settings = require('settings-sharelatex') +const Errors = require('../Errors/Errors') + +module.exports = UserController = { + tryDeleteUser(req, res, next) { + return UserController._tryDeleteUser(UserDeleter.deleteUser, req, res, next) + }, + + trySoftDeleteUser(req, res, next) { + return UserController._tryDeleteUser( + UserDeleter.softDeleteUser, + req, + res, + next + ) + }, + + _tryDeleteUser(deleteMethod, req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + const { password } = req.body + logger.log({ user_id }, 'trying to delete user account') + if (password == null || password === '') { + logger.err( + { user_id }, + 'no password supplied for attempt to delete account' + ) + return res.sendStatus(403) + } + return AuthenticationManager.authenticate( + { _id: user_id }, + password, + function(err, user) { + if (err != null) { + logger.err( + { user_id }, + 'error authenticating during attempt to delete account' + ) + return next(err) + } + if (!user) { + logger.err( + { user_id }, + 'auth failed during attempt to delete account' + ) + return res.sendStatus(403) + } + return deleteMethod(user_id, function(err) { + if (err != null) { + if (err instanceof Errors.SubscriptionAdminDeletionError) { + return res.status(422).json({ error: err.name }) + } else { + logger.err({ user_id }, 'error while deleting user account') + return next(err) + } + } + const sessionId = req.sessionID + if (typeof req.logout === 'function') { + req.logout() + } + return req.session.destroy(function(err) { + if (err != null) { + logger.err({ err }, 'error destorying session') + return next(err) + } + UserSessionsManager.untrackSession(user, sessionId) + return res.sendStatus(200) + }) + }) + } + ) + }, + + unsubscribe(req, res) { + const user_id = AuthenticationController.getLoggedInUserId(req) + return UserGetter.getUser(user_id, (err, user) => + newsLetterManager.unsubscribe(user, () => res.send()) + ) + }, + + updateUserSettings(req, res) { + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ user_id }, 'updating account settings') + return User.findById(user_id, function(err, user) { + if (err != null || user == null) { + logger.err({ err, user_id }, 'problem updaing user settings') + return res.sendStatus(500) + } + + if (req.body.first_name != null) { + user.first_name = req.body.first_name.trim() + } + if (req.body.last_name != null) { + user.last_name = req.body.last_name.trim() + } + if (req.body.role != null) { + user.role = req.body.role.trim() + } + if (req.body.institution != null) { + user.institution = req.body.institution.trim() + } + if (req.body.mode != null) { + user.ace.mode = req.body.mode + } + if (req.body.editorTheme != null) { + user.ace.theme = req.body.editorTheme + } + if (req.body.overallTheme != null) { + user.ace.overallTheme = req.body.overallTheme + } + if (req.body.fontSize != null) { + user.ace.fontSize = req.body.fontSize + } + if (req.body.autoComplete != null) { + user.ace.autoComplete = req.body.autoComplete + } + if (req.body.autoPairDelimiters != null) { + user.ace.autoPairDelimiters = req.body.autoPairDelimiters + } + if (req.body.spellCheckLanguage != null) { + user.ace.spellCheckLanguage = req.body.spellCheckLanguage + } + if (req.body.pdfViewer != null) { + user.ace.pdfViewer = req.body.pdfViewer + } + if (req.body.syntaxValidation != null) { + user.ace.syntaxValidation = req.body.syntaxValidation + } + if (req.body.fontFamily != null) { + user.ace.fontFamily = req.body.fontFamily + } + if (req.body.lineHeight != null) { + user.ace.lineHeight = req.body.lineHeight + } + + return user.save(function(err) { + const newEmail = + req.body.email != null + ? req.body.email.trim().toLowerCase() + : undefined + if ( + newEmail == null || + newEmail === user.email || + req.externalAuthenticationSystemUsed() + ) { + // end here, don't update email + AuthenticationController.setInSessionUser(req, { + first_name: user.first_name, + last_name: user.last_name + }) + return res.sendStatus(200) + } else if (newEmail.indexOf('@') === -1) { + // email invalid + return res.sendStatus(400) + } else { + // update the user email + return UserUpdater.changeEmailAddress(user_id, newEmail, function( + err + ) { + if (err != null) { + let message + logger.err( + { err, user_id, newEmail }, + 'problem updaing users email address' + ) + if (err instanceof Errors.EmailExistsError) { + message = req.i18n.translate('email_already_registered') + } else { + message = req.i18n.translate('problem_changing_email_address') + } + return res.send(500, { message }) + } + return User.findById(user_id, function(err, user) { + if (err != null) { + logger.err( + { err, user_id }, + 'error getting user for email update' + ) + return res.send(500) + } + AuthenticationController.setInSessionUser(req, { + email: user.email, + first_name: user.first_name, + last_name: user.last_name + }) + return UserHandler.populateTeamInvites(user, function(err) { + // need to refresh this in the background + if (err != null) { + logger.err({ err }, 'error populateTeamInvites') + } + return res.sendStatus(200) + }) + }) + }) + } + }) + }) + }, + + _doLogout(req, cb) { + if (cb == null) { + cb = function(err) {} + } + metrics.inc('user.logout') + const user = AuthenticationController.getSessionUser(req) + logger.log({ user }, 'logging out') + const sessionId = req.sessionID + if (typeof req.logout === 'function') { + req.logout() + } // passport logout + return req.session.destroy(function(err) { + if (err) { + logger.err({ err }, 'error destorying session') + cb(err) + } + if (user != null) { + UserSessionsManager.untrackSession(user, sessionId) + SudoModeHandler.clearSudoMode(user._id) + } + return cb() + }) + }, + + logout(req, res, next) { + return UserController._doLogout(req, function(err) { + if (err != null) { + return next(err) + } + const redirect_url = + settings.overleaf != null + ? settings.overleaf.host + '/users/ensure_signed_out' + : '/login' + return res.redirect(redirect_url) + }) + }, + + register(req, res, next) { + if (next == null) { + next = function(error) {} + } + const { email } = req.body + if (email == null || email === '') { + res.sendStatus(422) // Unprocessable Entity + return + } + return UserRegistrationHandler.registerNewUserAndSendActivationEmail( + email, + function(error, user, setNewPasswordUrl) { + if (error != null) { + return next(error) + } + return res.json({ + email: user.email, + setNewPasswordUrl + }) + } + ) + }, + + clearSessions(req, res, next) { + if (next == null) { + next = function(error) {} + } + metrics.inc('user.clear-sessions') + const user = AuthenticationController.getSessionUser(req) + logger.log({ user_id: user._id }, 'clearing sessions for user') + return UserSessionsManager.revokeAllUserSessions( + user, + [req.sessionID], + function(err) { + if (err != null) { + return next(err) + } + return res.sendStatus(201) + } + ) + }, + + changePassword(req, res, next) { + if (next == null) { + next = function(error) {} + } + metrics.inc('user.password-change') + const oldPass = req.body.currentPassword + const user_id = AuthenticationController.getLoggedInUserId(req) + return AuthenticationManager.authenticate( + { _id: user_id }, + oldPass, + function(err, user) { + if (err != null) { + return next(err) + } + if (user) { + logger.log({ user: user._id }, 'changing password') + const { newPassword1 } = req.body + const { newPassword2 } = req.body + const validationError = AuthenticationManager.validatePassword( + newPassword1 + ) + if (newPassword1 !== newPassword2) { + logger.log({ user }, 'passwords do not match') + return res.send({ + message: { + type: 'error', + text: 'Your passwords do not match' + } + }) + } else if (validationError != null) { + logger.log({ user }, validationError.message) + return res.send({ + message: { + type: 'error', + text: validationError.message + } + }) + } else { + logger.log({ user }, 'password changed') + return AuthenticationManager.setUserPassword( + user._id, + newPassword1, + function(error) { + if (error != null) { + return next(error) + } + return UserSessionsManager.revokeAllUserSessions( + user, + [req.sessionID], + function(err) { + if (err != null) { + return next(err) + } + return res.send({ + message: { + type: 'success', + text: 'Your password has been changed' + } + }) + } + ) + } + ) + } + } else { + logger.log({ user_id }, 'current password wrong') + return res.send({ + message: { + type: 'error', + text: 'Your old password is wrong' + } + }) + } + } + ) + } +} diff --git a/services/web/app/src/Features/User/UserCreator.js b/services/web/app/src/Features/User/UserCreator.js new file mode 100644 index 0000000000..b75a88ce6b --- /dev/null +++ b/services/web/app/src/Features/User/UserCreator.js @@ -0,0 +1,87 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserCreator +const { User } = require('../../models/User') +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const { addAffiliation } = require('../Institutions/InstitutionsAPI') + +module.exports = UserCreator = { + createNewUser(attributes, options, callback) { + if (callback == null) { + callback = function(error, user) {} + } + if (arguments.length === 2) { + callback = options + options = {} + } + logger.log({ user: attributes }, 'creating new user') + const user = new User() + + const username = attributes.email.match(/^[^@]*/) + if (attributes.first_name == null || attributes.first_name === '') { + attributes.first_name = username[0] + } + + for (let key in attributes) { + const value = attributes[key] + user[key] = value + } + + user.ace.syntaxValidation = true + if (user.featureSwitches != null) { + user.featureSwitches.pdfng = true + } + user.emails = [ + { + email: user.email, + createdAt: new Date(), + reversedHostname: user.email + .split('@')[1] + .split('') + .reverse() + .join('') + } + ] + + return user.save(function(err) { + callback(err, user) + + if (options != null ? options.skip_affiliation : undefined) { + return + } + // call addaffiliation after the main callback so it runs in the + // background. There is no guaranty this will run so we must no rely on it + return addAffiliation(user._id, user.email, function(error) { + if (error) { + return logger.log( + { userId: user._id, email: user.email, error }, + "couldn't add affiliation for user on create" + ) + } else { + return logger.log( + { userId: user._id, email: user.email }, + 'added affiliation for user on create' + ) + } + }) + }) + } +} + +metrics.timeAsyncMethod( + UserCreator, + 'createNewUser', + 'mongo.UserCreator', + logger +) diff --git a/services/web/app/src/Features/User/UserDeleter.js b/services/web/app/src/Features/User/UserDeleter.js new file mode 100644 index 0000000000..c83ca3c875 --- /dev/null +++ b/services/web/app/src/Features/User/UserDeleter.js @@ -0,0 +1,131 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserDeleter +const { User } = require('../../models/User') +const NewsletterManager = require('../Newsletter/NewsletterManager') +const ProjectDeleter = require('../Project/ProjectDeleter') +const logger = require('logger-sharelatex') +const SubscriptionHandler = require('../Subscription/SubscriptionHandler') +const SubscriptionUpdater = require('../Subscription/SubscriptionUpdater') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') +const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler') +const async = require('async') +const InstitutionsAPI = require('../Institutions/InstitutionsAPI') +const Errors = require('../Errors/Errors') +const { db, ObjectId } = require('../../infrastructure/mongojs') + +module.exports = UserDeleter = { + softDeleteUserForMigration(user_id, callback) { + if (callback == null) { + callback = function(err) {} + } + if (user_id == null) { + logger.err('user_id is null when trying to delete user') + return callback(new Error('no user_id')) + } + return User.findById(user_id, function(err, user) { + if (err != null) { + return callback(err) + } + if (user == null) { + return callback(new Errors.NotFoundError('user not found')) + } + return async.series( + [ + cb => UserDeleter._ensureCanDeleteUser(user, cb), + cb => UserDeleter._cleanupUser(user, cb), + cb => ProjectDeleter.deleteUsersProjects(user._id, cb), + function(cb) { + user.deletedAt = new Date() + return db.usersDeletedByMigration.insert(user, cb) + }, + cb => user.remove(cb) + ], + callback + ) + }) + }, + + deleteUser(user_id, callback) { + if (callback == null) { + callback = function() {} + } + if (user_id == null) { + logger.err('user_id is null when trying to delete user') + return callback('no user_id') + } + return User.findById(user_id, function(err, user) { + if (err != null) { + return callback(err) + } + logger.log({ user }, 'deleting user') + return async.series( + [ + cb => UserDeleter._ensureCanDeleteUser(user, cb), + cb => UserDeleter._cleanupUser(user, cb), + cb => ProjectDeleter.deleteUsersProjects(user._id, cb), + cb => user.remove(cb) + ], + function(err) { + if (err != null) { + logger.err( + { err, user_id }, + 'something went wrong deleteing the user' + ) + } + return callback(err) + } + ) + }) + }, + + _cleanupUser(user, callback) { + if (user == null) { + return callback(new Error('no user supplied')) + } + return async.series( + [ + cb => + NewsletterManager.unsubscribe(user, function(err) { + logger.err('Failed to unsubscribe user from newsletter', { + user_id: user._id, + error: err + }) + return cb() + }), + cb => SubscriptionHandler.cancelSubscription(user, cb), + cb => InstitutionsAPI.deleteAffiliations(user._id, cb), + cb => SubscriptionUpdater.removeUserFromAllGroups(user._id, cb), + cb => UserMembershipsHandler.removeUserFromAllEntities(user._id, cb) + ], + callback + ) + }, + + _ensureCanDeleteUser(user, callback) { + return SubscriptionLocator.getUsersSubscription(user, function( + error, + subscription + ) { + if (subscription != null) { + if (!error) { + error = new Errors.SubscriptionAdminDeletionError() + } + } + return callback(error) + }) + } +} diff --git a/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js new file mode 100644 index 0000000000..a466df67ea --- /dev/null +++ b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js @@ -0,0 +1,106 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserEmailsConfirmationHandler +const EmailHelper = require('../Helpers/EmailHelper') +const EmailHandler = require('../Email/EmailHandler') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const settings = require('settings-sharelatex') +const Errors = require('../Errors/Errors') +const logger = require('logger-sharelatex') +const UserUpdater = require('./UserUpdater') +const UserGetter = require('./UserGetter') + +const ONE_YEAR_IN_S = 365 * 24 * 60 * 60 + +module.exports = UserEmailsConfirmationHandler = { + sendConfirmationEmail(user_id, email, emailTemplate, callback) { + if (callback == null) { + callback = function(error) {} + } + if (arguments.length === 3) { + callback = emailTemplate + emailTemplate = 'confirmEmail' + } + + // when force-migrating accounts to v2 from v1, we don't want to send confirmation messages - + // setting this env var allows us to turn this behaviour off + if (process.env['SHARELATEX_NO_CONFIRMATION_MESSAGES'] != null) { + return callback(null) + } + + email = EmailHelper.parseEmail(email) + if (email == null) { + return callback(new Error('invalid email')) + } + const data = { user_id, email } + return OneTimeTokenHandler.getNewToken( + 'email_confirmation', + data, + { expiresIn: ONE_YEAR_IN_S }, + function(err, token) { + if (err != null) { + return callback(err) + } + const emailOptions = { + to: email, + confirmEmailUrl: `${ + settings.siteUrl + }/user/emails/confirm?token=${token}`, + sendingUser_id: user_id + } + return EmailHandler.sendEmail(emailTemplate, emailOptions, callback) + } + ) + }, + + confirmEmailFromToken(token, callback) { + if (callback == null) { + callback = function(error) {} + } + logger.log( + { token_start: token.slice(0, 8) }, + 'confirming email from token' + ) + return OneTimeTokenHandler.getValueFromTokenAndExpire( + 'email_confirmation', + token, + function(error, data) { + if (error != null) { + return callback(error) + } + if (data == null) { + return callback(new Errors.NotFoundError('no token found')) + } + const { user_id, email } = data + logger.log( + { data, user_id, email, token_start: token.slice(0, 8) }, + 'found data for email confirmation' + ) + if (user_id == null || email !== EmailHelper.parseEmail(email)) { + return callback(new Errors.NotFoundError('invalid data')) + } + return UserGetter.getUser(user_id, {}, function(error, user) { + if (error != null) { + return callback(error) + } + if (!(user != null ? user._id : undefined)) { + return callback(new Errors.NotFoundError('user not found')) + } + return UserUpdater.confirmEmail(user_id, email, callback) + }) + } + ) + } +} diff --git a/services/web/app/src/Features/User/UserEmailsController.js b/services/web/app/src/Features/User/UserEmailsController.js new file mode 100644 index 0000000000..9f564260e1 --- /dev/null +++ b/services/web/app/src/Features/User/UserEmailsController.js @@ -0,0 +1,211 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserEmailsController +const AuthenticationController = require('../Authentication/AuthenticationController') +const UserGetter = require('./UserGetter') +const UserUpdater = require('./UserUpdater') +const EmailHelper = require('../Helpers/EmailHelper') +const UserEmailsConfirmationHandler = require('./UserEmailsConfirmationHandler') +const { endorseAffiliation } = require('../Institutions/InstitutionsAPI') +const logger = require('logger-sharelatex') +const Errors = require('../Errors/Errors') + +module.exports = UserEmailsController = { + list(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + return UserGetter.getUserFullEmails(userId, function(error, fullEmails) { + if (error != null) { + return next(error) + } + return res.json(fullEmails) + }) + }, + + add(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.sendStatus(422) + } + + const affiliationOptions = { + university: req.body.university, + role: req.body.role, + department: req.body.department + } + return UserUpdater.addEmailAddress( + userId, + email, + affiliationOptions, + function(error) { + if (error != null) { + return UserEmailsController._handleEmailError(error, req, res, next) + } + return UserEmailsConfirmationHandler.sendConfirmationEmail( + userId, + email, + function(err) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + } + ) + }, + + remove(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.sendStatus(422) + } + + return UserUpdater.removeEmailAddress(userId, email, function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + }, + + setDefault(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.sendStatus(422) + } + + return UserUpdater.updateV1AndSetDefaultEmailAddress( + userId, + email, + function(error) { + if (error != null) { + return UserEmailsController._handleEmailError(error, req, res, next) + } else { + return res.sendStatus(200) + } + } + ) + }, + + endorse(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.sendStatus(422) + } + + return endorseAffiliation( + userId, + email, + req.body.role, + req.body.department, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + } + ) + }, + + resendConfirmation(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.sendStatus(422) + } + return UserGetter.getUserByAnyEmail(email, { _id: 1 }, function( + error, + user + ) { + if (error != null) { + return next(error) + } + if ( + user == null || + __guard__(user != null ? user._id : undefined, x => x.toString()) !== + userId + ) { + logger.log( + { userId, email, foundUserId: user != null ? user._id : undefined }, + "email doesn't match logged in user" + ) + return res.sendStatus(422) + } + logger.log({ userId, email }, 'resending email confirmation token') + return UserEmailsConfirmationHandler.sendConfirmationEmail( + userId, + email, + function(error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + } + ) + }) + }, + + showConfirm(req, res, next) { + return res.render('user/confirm_email', { + token: req.query.token, + title: 'confirm_email' + }) + }, + + confirm(req, res, next) { + const { token } = req.body + if (token == null) { + return res.sendStatus(422) + } + return UserEmailsConfirmationHandler.confirmEmailFromToken(token, function( + error + ) { + if (error != null) { + if (error instanceof Errors.NotFoundError) { + return res.status(404).json({ + message: + 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.' + }) + } else { + return next(error) + } + } else { + return res.sendStatus(200) + } + }) + }, + + _handleEmailError(error, req, res, next) { + if (error instanceof Errors.UnconfirmedEmailError) { + return res.status(409).json({ + message: 'email must be confirmed' + }) + } else if (error instanceof Errors.EmailExistsError) { + return res.status(409).json({ + message: req.i18n.translate('email_already_registered') + }) + } else { + return next(error) + } + } +} +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/User/UserGetter.js b/services/web/app/src/Features/User/UserGetter.js new file mode 100644 index 0000000000..4a4f7bd2ec --- /dev/null +++ b/services/web/app/src/Features/User/UserGetter.js @@ -0,0 +1,237 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserGetter +const mongojs = require('../../infrastructure/mongojs') +const metrics = require('metrics-sharelatex') +const logger = require('logger-sharelatex') +const { db } = mongojs +const { ObjectId } = mongojs +const { getUserAffiliations } = require('../Institutions/InstitutionsAPI') +const Errors = require('../Errors/Errors') + +module.exports = UserGetter = { + getUser(query, projection, callback) { + if (callback == null) { + callback = function(error, user) {} + } + if (query == null) { + return callback(new Error('no query provided')) + } + if ((query != null ? query.email : undefined) != null) { + return callback( + new Error("Don't use getUser to find user by email"), + null + ) + } + if (arguments.length === 2) { + callback = projection + projection = {} + } + if (typeof query === 'string') { + try { + query = { _id: ObjectId(query) } + } catch (e) { + return callback(null, null) + } + } else if (query instanceof ObjectId) { + query = { _id: query } + } + + return db.users.findOne(query, projection, callback) + }, + + getUserEmail(userId, callback) { + if (callback == null) { + callback = function(error, email) {} + } + return this.getUser(userId, { email: 1 }, (error, user) => + callback(error, user != null ? user.email : undefined) + ) + }, + + getUserFullEmails(userId, callback) { + if (callback == null) { + callback = function(error, emails) {} + } + return this.getUser(userId, { email: 1, emails: 1 }, function(error, user) { + if (error != null) { + return callback(error) + } + if (!user) { + return callback(new Error('User not Found')) + } + + return getUserAffiliations(userId, function(error, affiliationsData) { + if (error != null) { + return callback(error) + } + return callback( + null, + decorateFullEmails(user.email, user.emails || [], affiliationsData) + ) + }) + }) + }, + + getUserByMainEmail(email, projection, callback) { + if (callback == null) { + callback = function(error, user) {} + } + email = email.trim() + if (arguments.length === 2) { + callback = projection + projection = {} + } + return db.users.findOne({ email }, projection, callback) + }, + + getUserByAnyEmail(email, projection, callback) { + if (callback == null) { + callback = function(error, user) {} + } + email = email.trim() + if (arguments.length === 2) { + callback = projection + projection = {} + } + // $exists: true MUST be set to use the partial index + const query = { emails: { $exists: true }, 'emails.email': email } + return db.users.findOne(query, projection, (error, user) => { + if (error != null || user != null) { + return callback(error, user) + } + + // While multiple emails are being rolled out, check for the main email as + // well + return this.getUserByMainEmail(email, projection, callback) + }) + }, + + getUsersByAnyConfirmedEmail(emails, projection, callback) { + if (callback == null) { + callback = function(error, user) {} + } + if (arguments.length === 2) { + callback = projection + projection = {} + } + // $exists: true MUST be set to use the partial index + const query = { + emails: { + $exists: true, + $elemMatch: { email: { $in: emails }, confirmedAt: { $exists: true } } + } + } + return db.users.find(query, projection, (error, users) => { + return callback(error, users) + }) + }, + + getUsersByHostname(hostname, projection, callback) { + if (callback == null) { + callback = function(error, users) {} + } + const reversedHostname = hostname + .trim() + .split('') + .reverse() + .join('') + const query = { + emails: { $exists: true }, + 'emails.reversedHostname': reversedHostname + } + return db.users.find(query, projection, callback) + }, + + getUsers(user_ids, projection, callback) { + if (callback == null) { + callback = function(error, users) {} + } + try { + user_ids = user_ids.map(u => ObjectId(u.toString())) + } catch (error1) { + const error = error1 + return callback(error) + } + + return db.users.find({ _id: { $in: user_ids } }, projection, callback) + }, + + getUserOrUserStubById(user_id, projection, callback) { + let query + if (callback == null) { + callback = function(error, user, isStub) {} + } + try { + query = { _id: ObjectId(user_id.toString()) } + } catch (e) { + return callback(new Error(e)) + } + return db.users.findOne(query, projection, function(error, user) { + if (error != null) { + return callback(error) + } + if (user != null) { + return callback(null, user, false) + } + return db.userstubs.findOne(query, projection, function(error, user) { + if (error) { + return callback(error) + } + if (user == null) { + return callback() + } + return callback(null, user, true) + }) + }) + }, + + // check for duplicate email address. This is also enforced at the DB level + ensureUniqueEmailAddress(newEmail, callback) { + return this.getUserByAnyEmail(newEmail, function(error, user) { + if (user != null) { + return callback(new Errors.EmailExistsError('alread_exists')) + } + return callback(error) + }) + } +} + +var decorateFullEmails = (defaultEmail, emailsData, affiliationsData) => + emailsData.map(function(emailData) { + emailData.default = emailData.email === defaultEmail + + const affiliation = affiliationsData.find( + aff => aff.email === emailData.email + ) + if (affiliation != null) { + const { institution, inferred, role, department } = affiliation + emailData.affiliation = { institution, inferred, role, department } + } else { + emailsData.affiliation = null + } + + return emailData + }) +;[ + 'getUser', + 'getUserEmail', + 'getUserByMainEmail', + 'getUserByAnyEmail', + 'getUsers', + 'getUserOrUserStubById', + 'ensureUniqueEmailAddress' +].map(method => + metrics.timeAsyncMethod(UserGetter, method, 'mongo.UserGetter', logger) +) diff --git a/services/web/app/src/Features/User/UserHandler.js b/services/web/app/src/Features/User/UserHandler.js new file mode 100644 index 0000000000..e0419c9da0 --- /dev/null +++ b/services/web/app/src/Features/User/UserHandler.js @@ -0,0 +1,30 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserHandler +const TeamInvitesHandler = require('../Subscription/TeamInvitesHandler') + +module.exports = UserHandler = { + populateTeamInvites(user, callback) { + return TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail( + user.email, + callback + ) + }, + + setupLoginData(user, callback) { + if (callback == null) { + callback = function() {} + } + return this.populateTeamInvites(user, callback) + } +} diff --git a/services/web/app/src/Features/User/UserInfoController.js b/services/web/app/src/Features/User/UserInfoController.js new file mode 100644 index 0000000000..1ee22bfc06 --- /dev/null +++ b/services/web/app/src/Features/User/UserInfoController.js @@ -0,0 +1,120 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserController +const UserGetter = require('./UserGetter') +const logger = require('logger-sharelatex') +const UserDeleter = require('./UserDeleter') +const UserUpdater = require('./UserUpdater') +const sanitize = require('sanitizer') +const AuthenticationController = require('../Authentication/AuthenticationController') +const { ObjectId } = require('mongojs') + +module.exports = UserController = { + getLoggedInUsersPersonalInfo(req, res, next) { + if (next == null) { + next = function(error) {} + } + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log( + { user_id }, + 'reciving request for getting logged in users personal info' + ) + if (user_id == null) { + return next(new Error('User is not logged in')) + } + return UserGetter.getUser( + user_id, + { + first_name: true, + last_name: true, + role: true, + institution: true, + email: true, + signUpDate: true + }, + function(error, user) { + if (error != null) { + return next(error) + } + return UserController.sendFormattedPersonalInfo(user, res, next) + } + ) + }, + + getPersonalInfo(req, res, next) { + let query + if (next == null) { + next = function(error) {} + } + const { user_id } = req.params + + if (/^\d+$/.test(user_id)) { + query = { 'overleaf.id': parseInt(user_id, 10) } + } else if (/^[a-f0-9]{24}$/.test(user_id)) { + query = { _id: ObjectId(user_id) } + } else { + return res.send(400) + } + + return UserGetter.getUser( + query, + { _id: true, first_name: true, last_name: true, email: true }, + function(error, user) { + logger.log( + { user_id: req.params.user_id }, + 'receiving request for getting users personal info' + ) + if (error != null) { + return next(error) + } + if (user == null) { + return res.send(404) + } + return UserController.sendFormattedPersonalInfo(user, res, next) + } + ) + }, + + sendFormattedPersonalInfo(user, res, next) { + if (next == null) { + next = function(error) {} + } + const info = UserController.formatPersonalInfo(user) + return res.json(info) + }, + + formatPersonalInfo(user, callback) { + if (callback == null) { + callback = function(error, info) {} + } + if (user == null) { + return {} + } + const formatted_user = { id: user._id.toString() } + for (let key of [ + 'first_name', + 'last_name', + 'email', + 'signUpDate', + 'role', + 'institution' + ]) { + if (user[key] != null) { + formatted_user[key] = user[key] + } + } + return formatted_user + } +} diff --git a/services/web/app/src/Features/User/UserInfoManager.js b/services/web/app/src/Features/User/UserInfoManager.js new file mode 100644 index 0000000000..f5b733d973 --- /dev/null +++ b/services/web/app/src/Features/User/UserInfoManager.js @@ -0,0 +1,29 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserInfoManager +const UserGetter = require('./UserGetter') + +module.exports = UserInfoManager = { + getPersonalInfo(user_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return UserGetter.getUser( + user_id, + { _id: true, first_name: true, last_name: true, email: true }, + callback + ) + } +} diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js new file mode 100644 index 0000000000..3bfc47d530 --- /dev/null +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -0,0 +1,245 @@ +/* eslint-disable + camelcase, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserPagesController +const UserGetter = require('./UserGetter') +const UserSessionsManager = require('./UserSessionsManager') +const ErrorController = require('../Errors/ErrorController') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') +const request = require('request') +const fs = require('fs') +const AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = UserPagesController = { + registerPage(req, res) { + const sharedProjectData = { + project_name: req.query.project_name, + user_first_name: req.query.user_first_name + } + + const newTemplateData = {} + if (req.session.templateData != null) { + newTemplateData.templateName = req.session.templateData.templateName + } + + return res.render('user/register', { + title: 'register', + sharedProjectData, + newTemplateData, + new_email: req.query.new_email || '' + }) + }, + + 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 != null ? req.query.user_id : undefined) == null || + (req.query != null ? req.query.token : undefined) == null + ) { + return ErrorController.notFound(req, res) + } + + return UserGetter.getUser( + req.query.user_id, + { email: 1, loginCount: 1 }, + function(error, user) { + if (error != null) { + return next(error) + } + if (!user) { + return ErrorController.notFound(req, res) + } + if (user.loginCount > 0) { + logger.log( + { 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. + return res.redirect(`/login?email=${encodeURIComponent(user.email)}`) + } else { + return res.render('user/activate', { + title: 'activate_account', + email: user.email, + token: req.query.token + }) + } + } + ) + }, + + loginPage(req, res) { + // if user is being sent to /login with explicit redirect (redir=/foo), + // such as being sent from the editor to /login, then set the redirect explicitly + if ( + req.query.redir != null && + AuthenticationController._getRedirectFromSession(req) == null + ) { + logger.log( + { redir: req.query.redir }, + 'setting explicit redirect from login page' + ) + AuthenticationController.setRedirectInSession(req, req.query.redir) + } + return res.render('user/login', { + title: 'login', + email: req.query.email + }) + }, + + logoutPage(req, res) { + return res.render('user/logout') + }, + + renderReconfirmAccountPage(req, res) { + const page_data = { + reconfirm_email: __guard__( + req != null ? req.session : undefined, + x => x.reconfirm_email + ) + } + // when a user must reconfirm their account + return res.render('user/reconfirm', page_data) + }, + + settingsPage(req, res, next) { + const user_id = AuthenticationController.getLoggedInUserId(req) + logger.log({ user: user_id }, 'loading settings page') + const shouldAllowEditingDetails = + !__guard__( + Settings != null ? Settings.ldap : undefined, + x => x.updateUserDetailsOnLogin + ) && + !__guard__( + Settings != null ? Settings.saml : undefined, + x1 => x1.updateUserDetailsOnLogin + ) + const oauthProviders = Settings.oauthProviders || {} + + return UserGetter.getUser(user_id, function(err, user) { + if (err != null) { + return next(err) + } + + return UserPagesController._hasPassword(user, function( + err, + passwordPresent + ) { + if (err) { + logger.err({ err }, 'error getting password status from v1') + } + return res.render('user/settings', { + title: 'account_settings', + user, + hasPassword: passwordPresent, + shouldAllowEditingDetails, + languages: Settings.languages, + accountSettingsTabActive: true, + oauthProviders: UserPagesController._translateProviderDescriptions( + oauthProviders, + req + ), + thirdPartyIds: UserPagesController._restructureThirdPartyIds(user), + previewOauth: req.query.prvw != null + }) + }) + }) + }, + + sessionsPage(req, res, next) { + const user = AuthenticationController.getSessionUser(req) + logger.log({ user_id: user._id }, 'loading sessions page') + return UserSessionsManager.getAllUserSessions( + user, + [req.sessionID], + function(err, sessions) { + if (err != null) { + logger.err({ user_id: user._id }, 'error getting all user sessions') + return next(err) + } + return res.render('user/sessions', { + title: 'sessions', + sessions + }) + } + ) + }, + + _hasPassword(user, callback) { + return request.get( + { + url: `${Settings.apis.v1.url}/api/v1/sharelatex/has_password`, + auth: { user: Settings.apis.v1.user, pass: Settings.apis.v1.pass }, + body: { + user_id: __guard__( + user != null ? user.overleaf : undefined, + x => x.id + ) + }, + timeout: 20 * 1000, + json: true + }, + function(err, response, body) { + if (err) { + // for errors assume password and show password setting form + return callback(err, true) + } else if (body != null ? body.has_password : undefined) { + return callback(err, true) + } + return callback(err, false) + } + ) + }, + + _restructureThirdPartyIds(user) { + // 3rd party identifiers are an array of objects + // this turn them into a single object, which + // makes data easier to use in template + if ( + !user.thirdPartyIdentifiers || + user.thirdPartyIdentifiers.length === 0 + ) { + return null + } + return user.thirdPartyIdentifiers.reduce(function(obj, identifier) { + obj[identifier.providerId] = identifier.externalUserId + return obj + }, {}) + }, + + _translateProviderDescriptions(providers, req) { + const result = {} + if (providers) { + for (let provider in providers) { + const data = providers[provider] + data.description = req.i18n.translate( + data.descriptionKey, + data.descriptionOptions + ) + result[provider] = data + } + } + return result + } +} +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/User/UserRegistrationHandler.js b/services/web/app/src/Features/User/UserRegistrationHandler.js new file mode 100644 index 0000000000..23832514e4 --- /dev/null +++ b/services/web/app/src/Features/User/UserRegistrationHandler.js @@ -0,0 +1,163 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserRegistrationHandler +const { User } = require('../../models/User') +const UserCreator = require('./UserCreator') +const UserGetter = require('./UserGetter') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const NewsLetterManager = require('../Newsletter/NewsletterManager') +const async = require('async') +const logger = require('logger-sharelatex') +const crypto = require('crypto') +const EmailHandler = require('../Email/EmailHandler') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const Analytics = require('../Analytics/AnalyticsManager') +const settings = require('settings-sharelatex') +const EmailHelper = require('../Helpers/EmailHelper') + +module.exports = UserRegistrationHandler = { + _registrationRequestIsValid(body, callback) { + const invalidEmail = AuthenticationManager.validateEmail(body.email || '') + const invalidPassword = AuthenticationManager.validatePassword( + body.password || '' + ) + if (invalidEmail != null || invalidPassword != null) { + return false + } else { + return true + } + }, + + _createNewUserIfRequired(user, userDetails, callback) { + if (user == null) { + userDetails.holdingAccount = false + return UserCreator.createNewUser( + { + holdingAccount: false, + email: userDetails.email, + first_name: userDetails.first_name, + last_name: userDetails.last_name + }, + callback + ) + } else { + return callback(null, user) + } + }, + + registerNewUser(userDetails, callback) { + const self = this + const requestIsValid = this._registrationRequestIsValid(userDetails) + if (!requestIsValid) { + return callback(new Error('request is not valid')) + } + userDetails.email = EmailHelper.parseEmail(userDetails.email) + return UserGetter.getUserByAnyEmail(userDetails.email, (err, user) => { + if (err != null) { + return callback(err) + } + if ((user != null ? user.holdingAccount : undefined) === false) { + return callback(new Error('EmailAlreadyRegistered'), user) + } + return self._createNewUserIfRequired(user, userDetails, function( + err, + user + ) { + if (err != null) { + return callback(err) + } + return async.series( + [ + cb => + User.update( + { _id: user._id }, + { $set: { holdingAccount: false } }, + cb + ), + cb => + AuthenticationManager.setUserPassword( + user._id, + userDetails.password, + cb + ), + function(cb) { + if (userDetails.subscribeToNewsletter === 'true') { + NewsLetterManager.subscribe(user, function() {}) + } + return cb() + } // this can be slow, just fire it off + ], + function(err) { + logger.log({ user }, 'registered') + Analytics.recordEvent(user._id, 'user-registered') + return callback(err, user) + } + ) + }) + }) + }, + + registerNewUserAndSendActivationEmail(email, callback) { + if (callback == null) { + callback = function(error, user, setNewPasswordUrl) {} + } + logger.log({ email }, 'registering new user') + return UserRegistrationHandler.registerNewUser( + { + email, + password: crypto.randomBytes(32).toString('hex') + }, + function(err, user) { + if ( + err != null && + (err != null ? err.message : undefined) !== 'EmailAlreadyRegistered' + ) { + return callback(err) + } + + if ( + (err != null ? err.message : undefined) === 'EmailAlreadyRegistered' + ) { + logger.log({ email }, 'user already exists, resending welcome email') + } + + const ONE_WEEK = 7 * 24 * 60 * 60 // seconds + return OneTimeTokenHandler.getNewToken( + 'password', + user._id, + { expiresIn: ONE_WEEK }, + function(err, token) { + if (err != null) { + return callback(err) + } + + const setNewPasswordUrl = `${ + settings.siteUrl + }/user/activate?token=${token}&user_id=${user._id}` + + EmailHandler.sendEmail( + 'registered', + { + to: user.email, + setNewPasswordUrl + }, + function() {} + ) + + return callback(null, user, setNewPasswordUrl) + } + ) + } + ) + } +} diff --git a/services/web/app/src/Features/User/UserSessionsManager.js b/services/web/app/src/Features/User/UserSessionsManager.js new file mode 100644 index 0000000000..f07de5c30e --- /dev/null +++ b/services/web/app/src/Features/User/UserSessionsManager.js @@ -0,0 +1,294 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserSessionsManager +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const Async = require('async') +const _ = require('underscore') +const UserSessionsRedis = require('./UserSessionsRedis') +const rclient = UserSessionsRedis.client() + +module.exports = UserSessionsManager = { + // mimic the key used by the express sessions + _sessionKey(sessionId) { + return `sess:${sessionId}` + }, + + trackSession(user, sessionId, callback) { + if (callback == null) { + callback = function(err) {} + } + if (user == null) { + logger.log({ sessionId }, 'no user to track, returning') + return callback(null) + } + if (sessionId == null) { + logger.log({ user_id: user._id }, 'no sessionId to track, returning') + return callback(null) + } + logger.log({ user_id: user._id, sessionId }, 'onLogin handler') + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + const value = UserSessionsManager._sessionKey(sessionId) + return rclient + .multi() + .sadd(sessionSetKey, value) + .expire(sessionSetKey, `${Settings.cookieSessionLength}`) + .exec(function(err, response) { + if (err != null) { + logger.err( + { err, user_id: user._id, sessionSetKey }, + 'error while adding session key to UserSessions set' + ) + return callback(err) + } + UserSessionsManager._checkSessions(user, function() {}) + return callback() + }) + }, + + untrackSession(user, sessionId, callback) { + if (callback == null) { + callback = function(err) {} + } + if (user == null) { + logger.log({ sessionId }, 'no user to untrack, returning') + return callback(null) + } + if (sessionId == null) { + logger.log({ user_id: user._id }, 'no sessionId to untrack, returning') + return callback(null) + } + logger.log({ user_id: user._id, sessionId }, 'onLogout handler') + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + const value = UserSessionsManager._sessionKey(sessionId) + return rclient + .multi() + .srem(sessionSetKey, value) + .expire(sessionSetKey, `${Settings.cookieSessionLength}`) + .exec(function(err, response) { + if (err != null) { + logger.err( + { err, user_id: user._id, sessionSetKey }, + 'error while removing session key from UserSessions set' + ) + return callback(err) + } + UserSessionsManager._checkSessions(user, function() {}) + return callback() + }) + }, + + getAllUserSessions(user, exclude, callback) { + if (callback == null) { + callback = function(err, sessionKeys) {} + } + exclude = _.map(exclude, UserSessionsManager._sessionKey) + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + return rclient.smembers(sessionSetKey, function(err, sessionKeys) { + if (err != null) { + logger.err( + { user_id: user._id }, + 'error getting all session keys for user from redis' + ) + return callback(err) + } + sessionKeys = _.filter(sessionKeys, k => !_.contains(exclude, k)) + if (sessionKeys.length === 0) { + logger.log({ user_id: user._id }, 'no other sessions found, returning') + return callback(null, []) + } + + return Async.mapSeries( + sessionKeys, + (k, cb) => rclient.get(k, cb), + function(err, sessions) { + if (err != null) { + logger.err( + { user_id: user._id }, + 'error getting all sessions for user from redis' + ) + return callback(err) + } + + const result = [] + for (let session of Array.from(sessions)) { + if (session === null) { + continue + } + session = JSON.parse(session) + const session_user = + (session != null ? session.user : undefined) || + __guard__( + session != null ? session.passport : undefined, + x => x.user + ) + result.push({ + ip_address: session_user.ip_address, + session_created: session_user.session_created + }) + } + + return callback(null, result) + } + ) + }) + }, + + revokeAllUserSessions(user, retain, callback) { + if (callback == null) { + callback = function(err) {} + } + if (retain == null) { + retain = [] + } + retain = retain.map(i => UserSessionsManager._sessionKey(i)) + if (user == null) { + logger.log({}, 'no user to revoke sessions for, returning') + return callback(null) + } + logger.log({ user_id: user._id }, 'revoking all existing sessions for user') + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + return rclient.smembers(sessionSetKey, function(err, sessionKeys) { + if (err != null) { + logger.err( + { err, user_id: user._id, sessionSetKey }, + 'error getting contents of UserSessions set' + ) + return callback(err) + } + const keysToDelete = _.filter( + sessionKeys, + k => !Array.from(retain).includes(k) + ) + 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' + ) + + const deletions = keysToDelete.map(k => cb => rclient.del(k, cb)) + + return Async.series(deletions, function(err, _result) { + if (err != null) { + logger.err( + { err, user_id: user._id, sessionSetKey }, + 'errror revoking all sessions for user' + ) + return callback(err) + } + return rclient.srem(sessionSetKey, keysToDelete, function(err) { + if (err != null) { + logger.err( + { err, user_id: user._id, sessionSetKey }, + 'error removing session set for user' + ) + return callback(err) + } + return callback(null) + }) + }) + }) + }, + + touch(user, callback) { + if (callback == null) { + callback = function(err) {} + } + if (user == null) { + logger.log({}, 'no user to touch sessions for, returning') + return callback(null) + } + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + return rclient.expire( + sessionSetKey, + `${Settings.cookieSessionLength}`, + function(err, response) { + if (err != null) { + logger.err( + { err, user_id: user._id }, + 'error while updating ttl on UserSessions set' + ) + return callback(err) + } + return callback(null) + } + ) + }, + + _checkSessions(user, callback) { + if (callback == null) { + callback = function(err) {} + } + if (user == null) { + logger.log({}, 'no user, returning') + return callback(null) + } + logger.log({ user_id: user._id }, 'checking sessions for user') + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + return rclient.smembers(sessionSetKey, function(err, sessionKeys) { + if (err != null) { + 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' + ) + return Async.series( + sessionKeys.map(key => next => + rclient.get(key, function(err, val) { + if (err != null) { + return next(err) + } + if (val == null) { + logger.log( + { user_id: user._id, key }, + '>> removing key from UserSessions set' + ) + return rclient.srem(sessionSetKey, key, function(err, result) { + if (err != null) { + return next(err) + } + return next(null) + }) + } else { + return next() + } + }) + ), + function(err, results) { + logger.log({ user_id: user._id }, 'done checking sessions for user') + return callback(err) + } + ) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/User/UserSessionsRedis.js b/services/web/app/src/Features/User/UserSessionsRedis.js new file mode 100644 index 0000000000..19e324da1a --- /dev/null +++ b/services/web/app/src/Features/User/UserSessionsRedis.js @@ -0,0 +1,18 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +let Redis +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('websessions') + +module.exports = Redis = { + client() { + return rclient + }, + + sessionSetKey(user) { + return `UserSessions:{${user._id}}` + } +} diff --git a/services/web/app/src/Features/User/UserUpdater.js b/services/web/app/src/Features/User/UserUpdater.js new file mode 100644 index 0000000000..be89632d6b --- /dev/null +++ b/services/web/app/src/Features/User/UserUpdater.js @@ -0,0 +1,336 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-undef, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserUpdater +const logger = require('logger-sharelatex') +const mongojs = require('../../infrastructure/mongojs') +const metrics = require('metrics-sharelatex') +const { db } = mongojs +const async = require('async') +const { ObjectId } = mongojs +const UserGetter = require('./UserGetter') +const { + addAffiliation, + removeAffiliation +} = require('../Institutions/InstitutionsAPI') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') +const EmailHelper = require('../Helpers/EmailHelper') +const Errors = require('../Errors/Errors') +const Settings = require('settings-sharelatex') +const request = require('request') +const NewsletterManager = require('../Newsletter/NewsletterManager') + +module.exports = UserUpdater = { + updateUser(query, update, callback) { + if (callback == null) { + callback = function(error) {} + } + if (typeof query === 'string') { + query = { _id: ObjectId(query) } + } else if (query instanceof ObjectId) { + query = { _id: query } + } else if (typeof query._id === 'string') { + query._id = ObjectId(query._id) + } + + return db.users.update(query, update, callback) + }, + + // + // DEPRECATED + // + // Change the user's main email address by adding a new email, switching the + // default email and removing the old email. Prefer manipulating multiple + // emails and the default rather than calling this method directly + // + changeEmailAddress(userId, newEmail, callback) { + newEmail = EmailHelper.parseEmail(newEmail) + if (newEmail == null) { + return callback(new Error('invalid email')) + } + logger.log({ userId, newEmail }, 'updaing email address of user') + + let oldEmail = null + return async.series( + [ + cb => + UserGetter.getUserEmail(userId, function(error, email) { + oldEmail = email + return cb(error) + }), + cb => UserUpdater.addEmailAddress(userId, newEmail, cb), + cb => UserUpdater.setDefaultEmailAddress(userId, newEmail, cb), + cb => UserUpdater.removeEmailAddress(userId, oldEmail, cb) + ], + callback + ) + }, + + // Add a new email address for the user. Email cannot be already used by this + // or any other user + addEmailAddress(userId, newEmail, affiliationOptions, callback) { + if (callback == null) { + // affiliationOptions is optional + callback = affiliationOptions + affiliationOptions = {} + } + newEmail = EmailHelper.parseEmail(newEmail) + if (newEmail == null) { + return callback(new Error('invalid email')) + } + + return UserGetter.ensureUniqueEmailAddress(newEmail, error => { + if (error != null) { + return callback(error) + } + + return addAffiliation(userId, newEmail, affiliationOptions, error => { + if (error != null) { + logger.err({ error }, 'problem adding affiliation while adding email') + return callback(error) + } + + const reversedHostname = newEmail + .split('@')[1] + .split('') + .reverse() + .join('') + const update = { + $push: { + emails: { email: newEmail, createdAt: new Date(), reversedHostname } + } + } + return this.updateUser(userId, update, function(error) { + if (error != null) { + logger.err({ error }, 'problem updating users emails') + return callback(error) + } + return callback() + }) + }) + }) + }, + + // remove one of the user's email addresses. The email cannot be the user's + // default email address + removeEmailAddress(userId, email, callback) { + email = EmailHelper.parseEmail(email) + if (email == null) { + return callback(new Error('invalid email')) + } + return removeAffiliation(userId, email, error => { + if (error != null) { + logger.err({ error }, 'problem removing affiliation') + return callback(error) + } + + const query = { _id: userId, email: { $ne: email } } + const update = { $pull: { emails: { email } } } + return this.updateUser(query, update, function(error, res) { + if (error != null) { + logger.err({ error }, 'problem removing users email') + return callback(error) + } + if (res.n === 0) { + return callback(new Error('Cannot remove email')) + } + return callback() + }) + }) + }, + + // set the default email address by setting the `email` attribute. The email + // must be one of the user's multiple emails (`emails` attribute) + setDefaultEmailAddress(userId, email, callback) { + email = EmailHelper.parseEmail(email) + if (email == null) { + return callback(new Error('invalid email')) + } + return UserGetter.getUserEmail(userId, (error, oldEmail) => { + if (typeof err !== 'undefined' && err !== null) { + return callback(error) + } + const query = { _id: userId, 'emails.email': email } + const update = { $set: { email } } + return this.updateUser(query, update, function(error, res) { + if (error != null) { + logger.err({ error }, 'problem setting default emails') + return callback(error) + } else if (res.n === 0) { + // TODO: Check n or nMatched? + return callback(new Error('Default email does not belong to user')) + } else { + NewsletterManager.changeEmail(oldEmail, email, function() {}) + return callback() + } + }) + }) + }, + + updateV1AndSetDefaultEmailAddress(userId, email, callback) { + return this.updateEmailAddressInV1(userId, email, error => { + if (error != null) { + return callback(error) + } + return this.setDefaultEmailAddress(userId, email, callback) + }) + }, + + updateEmailAddressInV1(userId, newEmail, callback) { + if ( + __guard__( + Settings.apis != null ? Settings.apis.v1 : undefined, + x => x.url + ) == null + ) { + return callback() + } + return UserGetter.getUser(userId, { 'overleaf.id': 1, emails: 1 }, function( + error, + user + ) { + let email + if (error != null) { + return callback(error) + } + if (user == null) { + return callback(new Errors.NotFoundError('no user found')) + } + if ((user.overleaf != null ? user.overleaf.id : undefined) == null) { + return callback() + } + let newEmailIsConfirmed = false + for (email of Array.from(user.emails)) { + if (email.email === newEmail && email.confirmedAt != null) { + newEmailIsConfirmed = true + break + } + } + if (!newEmailIsConfirmed) { + return callback( + new Errors.UnconfirmedEmailError( + "can't update v1 with unconfirmed email" + ) + ) + } + return request( + { + baseUrl: Settings.apis.v1.url, + url: `/api/v1/sharelatex/users/${user.overleaf.id}/email`, + method: 'PUT', + auth: { + user: Settings.apis.v1.user, + pass: Settings.apis.v1.pass, + sendImmediately: true + }, + json: { + user: { + email: newEmail + } + }, + timeout: 5 * 1000 + }, + function(error, response, body) { + if (error != null) { + if (error.code === 'ECONNREFUSED') { + error = new Errors.V1ConnectionError('No V1 connection') + } + return callback(error) + } + if (response.statusCode === 409) { + // Conflict + return callback(new Errors.EmailExistsError('email exists in v1')) + } else if (response.statusCode >= 200 && response.statusCode < 300) { + return callback() + } else { + return callback( + new Error(`non-success code from v1: ${response.statusCode}`) + ) + } + } + ) + }) + }, + + confirmEmail(userId, email, confirmedAt, callback) { + if (arguments.length === 3) { + callback = confirmedAt + confirmedAt = new Date() + } + email = EmailHelper.parseEmail(email) + if (email == null) { + return callback(new Error('invalid email')) + } + logger.log({ userId, email }, 'confirming user email') + return addAffiliation(userId, email, { confirmedAt }, error => { + if (error != null) { + logger.err( + { error }, + 'problem adding affiliation while confirming email' + ) + return callback(error) + } + + const query = { + _id: userId, + 'emails.email': email + } + const update = { + $set: { + 'emails.$.confirmedAt': confirmedAt + } + } + return this.updateUser(query, update, function(error, res) { + if (error != null) { + return callback(error) + } + logger.log({ res, userId, email }, 'tried to confirm email') + if (res.n === 0) { + return callback( + new Errors.NotFoundError('user id and email do no match') + ) + } + return FeaturesUpdater.refreshFeatures(userId, true, callback) + }) + }) + }, + + removeReconfirmFlag(user_id, callback) { + return UserUpdater.updateUser( + user_id.toString(), + { + $set: { must_reconfirm: false } + }, + error => callback(error) + ) + } +} +;[ + 'updateUser', + 'changeEmailAddress', + 'setDefaultEmailAddress', + 'addEmailAddress', + 'removeEmailAddress', + 'removeReconfirmFlag' +].map(method => + metrics.timeAsyncMethod(UserUpdater, method, 'mongo.UserUpdater', logger) +) + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js b/services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js new file mode 100644 index 0000000000..e89af437ed --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js @@ -0,0 +1,312 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserMembershipAuthorization +const AuthenticationController = require('../Authentication/AuthenticationController') +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const UserMembershipHandler = require('./UserMembershipHandler') +const EntityConfigs = require('./UserMembershipEntityConfigs') +const Errors = require('../Errors/Errors') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const request = require('request') + +module.exports = UserMembershipAuthorization = { + requireTeamMetricsAccess(req, res, next) { + return requireAccessToEntity( + 'team', + req.params.id, + req, + res, + next, + 'groupMetrics' + ) + }, + + requireGroupManagementAccess(req, res, next) { + return requireAccessToEntity( + 'group', + req.params.id, + req, + res, + next, + 'groupManagement' + ) + }, + + requireGroupMetricsAccess(req, res, next) { + return requireAccessToEntity( + 'group', + req.params.id, + req, + res, + next, + 'groupMetrics' + ) + }, + + requireGroupManagersManagementAccess(req, res, next) { + return requireAccessToEntity( + 'groupManagers', + req.params.id, + req, + res, + next, + 'groupManagement' + ) + }, + + requireInstitutionMetricsAccess(req, res, next) { + return requireAccessToEntity( + 'institution', + req.params.id, + req, + res, + next, + 'institutionMetrics' + ) + }, + + requireInstitutionManagementAccess(req, res, next) { + return requireAccessToEntity( + 'institution', + req.params.id, + req, + res, + next, + 'institutionManagement' + ) + }, + + requirePublisherMetricsAccess(req, res, next) { + return requireAccessToEntity( + 'publisher', + req.params.id, + req, + res, + next, + 'publisherMetrics' + ) + }, + + requirePublisherManagementAccess(req, res, next) { + return requireAccessToEntity( + 'publisher', + req.params.id, + req, + res, + next, + 'publisherManagement' + ) + }, + + requireTemplateMetricsAccess(req, res, next) { + const templateId = req.params.id + return request( + { + baseUrl: settings.apis.v1.url, + url: `/api/v2/templates/${templateId}`, + method: 'GET', + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + sendImmediately: true + } + }, + (error, response, body) => { + if (response.statusCode === 404) { + return next(new Errors.NotFoundError()) + } + + if (response.statusCode !== 200) { + logger.err( + { templateId }, + "[TemplateMetrics] Couldn't fetch template data from v1" + ) + return next(new Error("Couldn't fetch template data from v1")) + } + + if (error != null) { + return next(error) + } + try { + body = JSON.parse(body) + } catch (error1) { + error = error1 + return next(error) + } + + req.template = { + id: body.id, + title: body.title + } + if (__guard__(body != null ? body.brand : undefined, x => x.slug)) { + req.params.id = body.brand.slug + return UserMembershipAuthorization.requirePublisherMetricsAccess( + req, + res, + next + ) + } else { + return AuthorizationMiddleware.ensureUserIsSiteAdmin(req, res, next) + } + } + ) + }, + + requireGraphAccess(req, res, next) { + req.params.id = req.query.resource_id + if (req.query.resource_type === 'template') { + return UserMembershipAuthorization.requireTemplateMetricsAccess( + req, + res, + next + ) + } else if (req.query.resource_type === 'institution') { + return UserMembershipAuthorization.requireInstitutionMetricsAccess( + req, + res, + next + ) + } else if (req.query.resource_type === 'group') { + return UserMembershipAuthorization.requireGroupMetricsAccess( + req, + res, + next + ) + } else if (req.query.resource_type === 'team') { + return UserMembershipAuthorization.requireTeamMetricsAccess( + req, + res, + next + ) + } + return requireAccessToEntity( + req.query.resource_type, + req.query.resource_id, + req, + res, + next + ) + }, + + requireEntityCreationAccess(req, res, next) { + const loggedInUser = AuthenticationController.getSessionUser(req) + if (!loggedInUser || !hasEntityCreationAccess(loggedInUser)) { + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + return next() + } +} + +var requireAccessToEntity = function( + entityName, + entityId, + req, + res, + next, + requiredStaffAccess = null +) { + const loggedInUser = AuthenticationController.getSessionUser(req) + if (!loggedInUser) { + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + + return getEntity( + entityName, + entityId, + loggedInUser, + requiredStaffAccess, + function(error, entity, entityConfig, entityExists) { + if (error != null) { + return next(error) + } + + if (entity != null) { + req.entity = entity + req.entityConfig = entityConfig + return next() + } + + if (entityExists) { + // user doesn't have access to entity + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + + if (hasEntityCreationAccess(loggedInUser) && entityConfig.canCreate) { + // entity doesn't exists, admin can create it + return res.redirect(`/entities/${entityName}/create/${entityId}`) + } + + return next(new Errors.NotFoundError()) + } + ) +} + +var getEntity = function( + entityName, + entityId, + user, + requiredStaffAccess, + callback +) { + if (callback == null) { + callback = function(error, entity, entityConfig, entityExists) {} + } + const entityConfig = EntityConfigs[entityName] + if (!entityConfig) { + return callback(new Errors.NotFoundError(`No such entity: ${entityName}`)) + } + + return UserMembershipHandler.getEntity( + entityId, + entityConfig, + user, + requiredStaffAccess, + function(error, entity) { + if (error != null) { + return callback(error) + } + if (entity != null) { + return callback(null, entity, entityConfig, true) + } + + // no access to entity. Check if entity exists + return UserMembershipHandler.getEntityWithoutAuthorizationCheck( + entityId, + entityConfig, + function(error, entity) { + if (error != null) { + return callback(error) + } + return callback(null, null, entityConfig, entity != null) + } + ) + } + ) +} + +var hasEntityCreationAccess = user => + user.isAdmin || + (user.staffAccess != null + ? user.staffAccess['institutionManagement'] + : undefined) || + (user.staffAccess != null + ? user.staffAccess['publisherManagement'] + : undefined) + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipController.js b/services/web/app/src/Features/UserMembership/UserMembershipController.js new file mode 100644 index 0000000000..ddc9f9a110 --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipController.js @@ -0,0 +1,184 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthenticationController = require('../Authentication/AuthenticationController') +const UserMembershipHandler = require('./UserMembershipHandler') +const EntityConfigs = require('./UserMembershipEntityConfigs') +const Errors = require('../Errors/Errors') +const EmailHelper = require('../Helpers/EmailHelper') +const logger = require('logger-sharelatex') + +module.exports = { + index(req, res, next) { + const { entity, entityConfig } = req + return entity.fetchV1Data(function(error, entity) { + if (error != null) { + return next(error) + } + return UserMembershipHandler.getUsers(entity, entityConfig, function( + error, + users + ) { + let entityName + if (error != null) { + return next(error) + } + const entityPrimaryKey = entity[ + entityConfig.fields.primaryKey + ].toString() + if (entityConfig.fields.name) { + entityName = entity[entityConfig.fields.name] + } + return res.render('user_membership/index', { + name: entityName, + users, + groupSize: entityConfig.hasMembersLimit + ? entity.membersLimit + : undefined, + translations: entityConfig.translations, + paths: entityConfig.pathsFor(entityPrimaryKey) + }) + }) + }) + }, + + add(req, res, next) { + const { entity, entityConfig } = req + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.status(400).json({ + error: { + code: 'invalid_email', + message: req.i18n.translate('invalid_email') + } + }) + } + + if (entityConfig.readOnly) { + return next(new Errors.NotFoundError('Cannot add users to entity')) + } + + return UserMembershipHandler.addUser(entity, entityConfig, email, function( + error, + user + ) { + if (error != null ? error.alreadyAdded : undefined) { + return res.status(400).json({ + error: { + code: 'user_already_added', + message: req.i18n.translate('user_already_added') + } + }) + } + if (error != null ? error.userNotFound : undefined) { + return res.status(404).json({ + error: { + code: 'user_not_found', + message: req.i18n.translate('user_not_found') + } + }) + } + if (error != null) { + return next(error) + } + return res.json({ user }) + }) + }, + + remove(req, res, next) { + const { entity, entityConfig } = req + const { userId } = req.params + + if (entityConfig.readOnly) { + return next(new Errors.NotFoundError('Cannot remove users from entity')) + } + + const loggedInUserId = AuthenticationController.getLoggedInUserId(req) + if (loggedInUserId === userId) { + return res.status(400).json({ + error: { + code: 'managers_cannot_remove_self', + message: req.i18n.translate('managers_cannot_remove_self') + } + }) + } + + return UserMembershipHandler.removeUser( + entity, + entityConfig, + userId, + function(error, user) { + if (error != null ? error.isAdmin : undefined) { + return res.status(400).json({ + error: { + code: 'managers_cannot_remove_admin', + message: req.i18n.translate('managers_cannot_remove_admin') + } + }) + } + if (error != null) { + return next(error) + } + return res.send() + } + ) + }, + + exportCsv(req, res, next) { + const { entity, entityConfig } = req + logger.log({ subscriptionId: entity._id }, 'exporting csv') + return UserMembershipHandler.getUsers(entity, entityConfig, function( + error, + users + ) { + if (error != null) { + return next(error) + } + let csvOutput = '' + for (let user of Array.from(users)) { + csvOutput += user.email + '\n' + } + res.header('Content-Disposition', 'attachment; filename=Group.csv') + res.contentType('text/csv') + return res.send(csvOutput) + }) + }, + + new(req, res, next) { + return res.render('user_membership/new', { + entityName: req.params.name, + entityId: req.params.id + }) + }, + + create(req, res, next) { + const entityName = req.params.name + const entityId = req.params.id + const entityConfig = EntityConfigs[entityName] + if (!entityConfig) { + return next(new Errors.NotFoundError(`No such entity: ${entityName}`)) + } + if (!entityConfig.canCreate) { + return next(new Errors.NotFoundError(`Cannot create new ${entityName}`)) + } + + return UserMembershipHandler.createEntity(entityId, entityConfig, function( + error, + entity + ) { + if (error != null) { + return next(error) + } + return res.redirect(entityConfig.pathsFor(entityId).index) + }) + } +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipEntityConfigs.js b/services/web/app/src/Features/UserMembership/UserMembershipEntityConfigs.js new file mode 100644 index 0000000000..c24ad6d682 --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipEntityConfigs.js @@ -0,0 +1,122 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +module.exports = { + group: { + modelName: 'Subscription', + readOnly: true, + hasMembersLimit: true, + fields: { + primaryKey: '_id', + read: ['invited_emails', 'teamInvites', 'member_ids'], + write: null, + access: 'manager_ids', + name: 'teamName' + }, + baseQuery: { + groupPlan: true + }, + translations: { + title: 'group_account', + subtitle: 'members_management', + remove: 'remove_from_group' + }, + pathsFor(id) { + return { + addMember: `/manage/groups/${id}/invites`, + removeMember: `/manage/groups/${id}/user`, + removeInvite: `/manage/groups/${id}/invites`, + exportMembers: `/manage/groups/${id}/members/export` + } + } + }, + + team: { + // for metrics only + modelName: 'Subscription', + fields: { + primaryKey: 'overleaf.id', + access: 'manager_ids' + }, + baseQuery: { + groupPlan: true + } + }, + + groupManagers: { + modelName: 'Subscription', + fields: { + primaryKey: '_id', + read: ['manager_ids'], + write: 'manager_ids', + access: 'manager_ids', + name: 'teamName' + }, + baseQuery: { + groupPlan: true + }, + translations: { + title: 'group_account', + subtitle: 'managers_management', + remove: 'remove_manager' + }, + pathsFor(id) { + return { + addMember: `/manage/groups/${id}/managers`, + removeMember: `/manage/groups/${id}/managers` + } + } + }, + + institution: { + modelName: 'Institution', + canCreate: true, + fields: { + primaryKey: 'v1Id', + read: ['managerIds'], + write: 'managerIds', + access: 'managerIds', + name: 'name' + }, + translations: { + title: 'institution_account', + subtitle: 'managers_management', + remove: 'remove_manager' + }, + pathsFor(id) { + return { + index: `/manage/institutions/${id}/managers`, + addMember: `/manage/institutions/${id}/managers`, + removeMember: `/manage/institutions/${id}/managers` + } + } + }, + + publisher: { + modelName: 'Publisher', + canCreate: true, + fields: { + primaryKey: 'slug', + read: ['managerIds'], + write: 'managerIds', + access: 'managerIds', + name: 'name' + }, + translations: { + title: 'publisher_account', + subtitle: 'managers_management', + remove: 'remove_manager' + }, + pathsFor(id) { + return { + index: `/manage/publishers/${id}/managers`, + addMember: `/manage/publishers/${id}/managers`, + removeMember: `/manage/publishers/${id}/managers` + } + } + } +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipHandler.js b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js new file mode 100644 index 0000000000..da4a6517ef --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js @@ -0,0 +1,154 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { ObjectId } = require('mongoose').Types +const async = require('async') +const Errors = require('../Errors/Errors') +const EntityModels = { + Institution: require('../../models/Institution').Institution, + Subscription: require('../../models/Subscription').Subscription, + Publisher: require('../../models/Publisher').Publisher +} +const UserMembershipViewModel = require('./UserMembershipViewModel') +const UserGetter = require('../User/UserGetter') +const logger = require('logger-sharelatex') +const UserMembershipEntityConfigs = require('./UserMembershipEntityConfigs') + +module.exports = { + getEntity( + entityId, + entityConfig, + loggedInUser, + requiredStaffAccess, + callback + ) { + if (callback == null) { + callback = function(error, entity) {} + } + const query = buildEntityQuery(entityId, entityConfig) + if ( + !loggedInUser.isAdmin && + !(loggedInUser.staffAccess != null + ? loggedInUser.staffAccess[requiredStaffAccess] + : undefined) + ) { + query[entityConfig.fields.access] = ObjectId(loggedInUser._id) + } + return EntityModels[entityConfig.modelName].findOne(query, callback) + }, + + getEntityWithoutAuthorizationCheck(entityId, entityConfig, callback) { + if (callback == null) { + callback = function(error, entity) {} + } + const query = buildEntityQuery(entityId, entityConfig) + return EntityModels[entityConfig.modelName].findOne(query, callback) + }, + + createEntity(entityId, entityConfig, callback) { + if (callback == null) { + callback = function(error, entity) {} + } + const data = buildEntityQuery(entityId, entityConfig) + return EntityModels[entityConfig.modelName].create(data, callback) + }, + + getUsers(entity, entityConfig, callback) { + if (callback == null) { + callback = function(error, users) {} + } + const attributes = entityConfig.fields.read + return getPopulatedListOfMembers(entity, attributes, callback) + }, + + addUser(entity, entityConfig, email, callback) { + if (callback == null) { + callback = function(error, user) {} + } + const attribute = entityConfig.fields.write + return UserGetter.getUserByAnyEmail(email, function(error, user) { + if (error != null) { + return callback(error) + } + if (!user) { + return callback({ userNotFound: true }) + } + if (entity[attribute].some(managerId => managerId.equals(user._id))) { + return callback({ alreadyAdded: true }) + } + + return addUserToEntity(entity, attribute, user, error => + callback(error, UserMembershipViewModel.build(user)) + ) + }) + }, + + removeUser(entity, entityConfig, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + const attribute = entityConfig.fields.write + if (entity.admin_id != null ? entity.admin_id.equals(userId) : undefined) { + return callback({ isAdmin: true }) + } + return removeUserFromEntity(entity, attribute, userId, callback) + } +} + +var getPopulatedListOfMembers = function(entity, attributes, callback) { + if (callback == null) { + callback = function(error, users) {} + } + const userObjects = [] + + for (let attribute of Array.from(attributes)) { + for (let userObject of Array.from(entity[attribute] || [])) { + // userObject can be an email as String, a user id as ObjectId or an + // invite as Object with an email attribute as String. We want to pass to + // UserMembershipViewModel either an email as (String) or a user id (ObjectId) + const userIdOrEmail = userObject.email || userObject + userObjects.push(userIdOrEmail) + } + } + + return async.map(userObjects, UserMembershipViewModel.buildAsync, callback) +} + +var addUserToEntity = function(entity, attribute, user, callback) { + if (callback == null) { + callback = function(error) {} + } + const fieldUpdate = {} + fieldUpdate[attribute] = user._id + return entity.update({ $addToSet: fieldUpdate }, callback) +} + +var removeUserFromEntity = function(entity, attribute, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + const fieldUpdate = {} + fieldUpdate[attribute] = userId + return entity.update({ $pull: fieldUpdate }, callback) +} + +var buildEntityQuery = function(entityId, entityConfig, loggedInUser) { + if (ObjectId.isValid(entityId.toString())) { + entityId = ObjectId(entityId) + } + const query = Object.assign({}, entityConfig.baseQuery) + query[entityConfig.fields.primaryKey] = entityId + return query +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipRouter.js b/services/web/app/src/Features/UserMembership/UserMembershipRouter.js new file mode 100644 index 0000000000..702eb34d1b --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipRouter.js @@ -0,0 +1,116 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const UserMembershipAuthorization = require('./UserMembershipAuthorization') +const UserMembershipController = require('./UserMembershipController') +const SubscriptionGroupController = require('../Subscription/SubscriptionGroupController') +const TeamInvitesController = require('../Subscription/TeamInvitesController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') + +module.exports = { + apply(webRouter) { + // group members routes + webRouter.get( + '/manage/groups/:id/members', + UserMembershipAuthorization.requireGroupManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/groups/:id/invites', + UserMembershipAuthorization.requireGroupManagementAccess, + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-team-invite', + maxRequests: 100, + timeInterval: 60 + }), + TeamInvitesController.createInvite + ) + webRouter.delete( + '/manage/groups/:id/user/:user_id', + UserMembershipAuthorization.requireGroupManagementAccess, + SubscriptionGroupController.removeUserFromGroup + ) + webRouter.delete( + '/manage/groups/:id/invites/:email', + UserMembershipAuthorization.requireGroupManagementAccess, + TeamInvitesController.revokeInvite + ) + webRouter.get( + '/manage/groups/:id/members/export', + UserMembershipAuthorization.requireGroupManagementAccess, + RateLimiterMiddleware.rateLimit({ + endpointName: 'export-team-csv', + maxRequests: 30, + timeInterval: 60 + }), + UserMembershipController.exportCsv + ) + + // group managers routes + webRouter.get( + '/manage/groups/:id/managers', + UserMembershipAuthorization.requireGroupManagersManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/groups/:id/managers', + UserMembershipAuthorization.requireGroupManagersManagementAccess, + UserMembershipController.add + ) + webRouter.delete( + '/manage/groups/:id/managers/:userId', + UserMembershipAuthorization.requireGroupManagersManagementAccess, + UserMembershipController.remove + ) + + // institution members routes + webRouter.get( + '/manage/institutions/:id/managers', + UserMembershipAuthorization.requireInstitutionManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/institutions/:id/managers', + UserMembershipAuthorization.requireInstitutionManagementAccess, + UserMembershipController.add + ) + webRouter.delete( + '/manage/institutions/:id/managers/:userId', + UserMembershipAuthorization.requireInstitutionManagementAccess, + UserMembershipController.remove + ) + + // publisher members routes + webRouter.get( + '/manage/publishers/:id/managers', + UserMembershipAuthorization.requirePublisherManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/publishers/:id/managers', + UserMembershipAuthorization.requirePublisherManagementAccess, + UserMembershipController.add + ) + webRouter.delete( + '/manage/publishers/:id/managers/:userId', + UserMembershipAuthorization.requirePublisherManagementAccess, + UserMembershipController.remove + ) + + // create new entitites + webRouter.get( + '/entities/:name/create/:id', + UserMembershipAuthorization.requireEntityCreationAccess, + UserMembershipController.new + ) + return webRouter.post( + '/entities/:name/create/:id', + UserMembershipAuthorization.requireEntityCreationAccess, + UserMembershipController.create + ) + } +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js new file mode 100644 index 0000000000..120d109d2a --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js @@ -0,0 +1,72 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserMembershipViewModel +const { ObjectId } = require('mongojs') +const UserGetter = require('../User/UserGetter') + +module.exports = UserMembershipViewModel = { + build(userOrEmail) { + if (userOrEmail._id) { + return buildUserViewModel(userOrEmail) + } else { + return buildUserViewModelWithEmail(userOrEmail) + } + }, + + buildAsync(userOrIdOrEmail, callback) { + if (callback == null) { + callback = function(error, viewModel) {} + } + if (!(userOrIdOrEmail instanceof ObjectId)) { + // userOrIdOrEmail is a user or an email and can be parsed by #build + return callback(null, UserMembershipViewModel.build(userOrIdOrEmail)) + } + + const userId = userOrIdOrEmail + const projection = { email: 1, first_name: 1, last_name: 1 } + return UserGetter.getUserOrUserStubById(userId, projection, function( + error, + user, + isStub + ) { + if (error != null || user == null) { + return callback(null, buildUserViewModelWithId(userId.toString())) + } + if (isStub) { + return callback(null, buildUserViewModelWithStub(user)) + } + return callback(null, buildUserViewModel(user)) + }) + } +} + +var buildUserViewModel = function(user, isInvite) { + if (isInvite == null) { + isInvite = false + } + return { + _id: user._id || null, + email: user.email || null, + first_name: user.first_name || null, + last_name: user.last_name || null, + invite: isInvite + } +} + +var buildUserViewModelWithEmail = email => buildUserViewModel({ email }, true) + +var buildUserViewModelWithStub = user => + // user stubs behave as invites + buildUserViewModel(user, true) + +var buildUserViewModelWithId = id => buildUserViewModel({ _id: id }, false) diff --git a/services/web/app/src/Features/UserMembership/UserMembershipsHandler.js b/services/web/app/src/Features/UserMembership/UserMembershipsHandler.js new file mode 100644 index 0000000000..c524e51015 --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipsHandler.js @@ -0,0 +1,85 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserMembershipsHandler +const async = require('async') +const EntityModels = { + Institution: require('../../models/Institution').Institution, + Subscription: require('../../models/Subscription').Subscription, + Publisher: require('../../models/Publisher').Publisher +} +const UserMembershipEntityConfigs = require('./UserMembershipEntityConfigs') + +module.exports = UserMembershipsHandler = { + removeUserFromAllEntities(userId, callback) { + // get all writable entity types + if (callback == null) { + callback = function(error) {} + } + const entityConfigs = [] + for (let key in UserMembershipEntityConfigs) { + const entityConfig = UserMembershipEntityConfigs[key] + if (entityConfig.fields.write != null) { + entityConfigs.push(entityConfig) + } + } + + // remove the user from all entities types + return async.map( + entityConfigs, + (entityConfig, innerCallback) => + UserMembershipsHandler.removeUserFromEntities( + entityConfig, + userId, + innerCallback + ), + callback + ) + }, + + removeUserFromEntities(entityConfig, userId, callback) { + if (callback == null) { + callback = function(error) {} + } + const removeOperation = { $pull: {} } + removeOperation['$pull'][entityConfig.fields.write] = userId + return EntityModels[entityConfig.modelName].updateMany( + {}, + removeOperation, + callback + ) + }, + + getEntitiesByUser(entityConfig, userId, callback) { + if (callback == null) { + callback = function(error, entities) {} + } + const query = Object.assign({}, entityConfig.baseQuery) + query[entityConfig.fields.access] = userId + return EntityModels[entityConfig.modelName].find(query, function( + error, + entities + ) { + if (entities == null) { + entities = [] + } + if (error != null) { + return callback(error) + } + return async.mapSeries( + entities, + (entity, cb) => entity.fetchV1Data(cb), + callback + ) + }) + } +} diff --git a/services/web/app/src/Features/V1/V1Api.js b/services/web/app/src/Features/V1/V1Api.js new file mode 100644 index 0000000000..22c12e4320 --- /dev/null +++ b/services/web/app/src/Features/V1/V1Api.js @@ -0,0 +1,106 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let V1Api +const request = require('request') +const settings = require('settings-sharelatex') +const Errors = require('../Errors/Errors') + +// TODO: check what happens when these settings aren't defined +const DEFAULT_V1_PARAMS = { + baseUrl: __guard__( + __guard__(settings != null ? settings.apis : undefined, x1 => x1.v1), + x => x.url + ), + auth: { + user: __guard__( + __guard__(settings != null ? settings.apis : undefined, x3 => x3.v1), + x2 => x2.user + ), + pass: __guard__( + __guard__(settings != null ? settings.apis : undefined, x5 => x5.v1), + x4 => x4.pass + ) + }, + json: true, + timeout: 30 * 1000 +} + +const v1Request = request.defaults(DEFAULT_V1_PARAMS) + +const DEFAULT_V1_OAUTH_PARAMS = { + baseUrl: __guard__( + __guard__(settings != null ? settings.apis : undefined, x7 => x7.v1), + x6 => x6.url + ), + json: true, + timeout: 30 * 1000 +} + +const v1OauthRequest = request.defaults(DEFAULT_V1_OAUTH_PARAMS) + +module.exports = V1Api = { + request(options, callback) { + if (callback == null) { + return request(options) + } + return v1Request(options, (error, response, body) => + V1Api._responseHandler(options, error, response, body, callback) + ) + }, + + oauthRequest(options, token, callback) { + if (options.uri == null) { + return callback(new Error('uri required')) + } + if (options.method == null) { + options.method = 'GET' + } + options.auth = { bearer: token } + return v1OauthRequest(options, (error, response, body) => + V1Api._responseHandler(options, error, response, body, callback) + ) + }, + + _responseHandler(options, error, response, body, callback) { + if (error != null) { + return callback(error, response, body) + } + if ( + (response.statusCode >= 200 && response.statusCode < 300) || + Array.from(options.expectedStatusCodes || []).includes( + response.statusCode + ) + ) { + return callback(null, response, body) + } else if (response.statusCode === 403) { + error = new Errors.ForbiddenError('overleaf v1 returned forbidden') + error.statusCode = response.statusCode + return callback(error) + } else { + error = new Error( + `overleaf v1 returned non-success code: ${response.statusCode} ${ + options.method + } ${options.uri}` + ) + error.statusCode = response.statusCode + return callback(error) + } + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/V1/V1Handler.js b/services/web/app/src/Features/V1/V1Handler.js new file mode 100644 index 0000000000..dc0bdecd7f --- /dev/null +++ b/services/web/app/src/Features/V1/V1Handler.js @@ -0,0 +1,135 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let V1Handler +const V1Api = require('./V1Api') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') + +module.exports = V1Handler = { + authWithV1(email, password, callback) { + if (callback == null) { + callback = function(err, isValid, v1Profile) {} + } + return V1Api.request( + { + method: 'POST', + url: '/api/v1/sharelatex/login', + json: { email, password }, + expectedStatusCodes: [403] + }, + function(err, response, body) { + if (err != null) { + logger.err( + { email, err }, + '[V1Handler] error while talking to v1 login api' + ) + return callback(err) + } + if ([200, 403].includes(response.statusCode)) { + const isValid = body.valid + const userProfile = body.user_profile + logger.log( + { + email, + isValid, + v1UserId: __guard__( + body != null ? body.user_profile : undefined, + x => x.id + ) + }, + '[V1Handler] got response from v1 login api' + ) + return callback(null, isValid, userProfile) + } else { + err = new Error( + `Unexpected status from v1 login api: ${response.statusCode}` + ) + return callback(err) + } + } + ) + }, + + doPasswordReset(v1_user_id, password, callback) { + if (callback == null) { + callback = function(err, created) {} + } + logger.log({ v1_user_id }, 'sending password reset request to v1 login api') + return V1Api.request( + { + method: 'POST', + url: '/api/v1/sharelatex/reset_password', + json: { + user_id: v1_user_id, + password + }, + expectedStatusCodes: [200] + }, + function(err, response, body) { + if (err != null) { + logger.err( + { v1_user_id, err }, + 'error while talking to v1 password reset api' + ) + return callback(err, false) + } + if ([200].includes(response.statusCode)) { + logger.log( + { v1_user_id, changed: true }, + 'got success response from v1 password reset api' + ) + return callback(null, true) + } else { + err = new Error( + `Unexpected status from v1 password reset api: ${ + response.statusCode + }` + ) + return callback(err, false) + } + } + ) + }, + + getDocExported(token, callback) { + // default to not exported + if (callback == null) { + callback = function(err, info) {} + } + if ((Settings.apis != null ? Settings.apis.v1 : undefined) == null) { + return callback(null, { + exported: false, + exporting: false + }) + } + + return V1Api.request( + { url: `/api/v1/sharelatex/docs/${token}/exported_to_v2` }, + function(err, response, body) { + if (err != null) { + return callback(err) + } + return callback(null, body) + } + ) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/infrastructure/CrawlerLogger.js b/services/web/app/src/infrastructure/CrawlerLogger.js new file mode 100644 index 0000000000..06932c7258 --- /dev/null +++ b/services/web/app/src/infrastructure/CrawlerLogger.js @@ -0,0 +1,23 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const metrics = require('metrics-sharelatex') +module.exports = { + log(req) { + if (req.headers['user-agent'] != null) { + const userAgent = req.headers['user-agent'].toLowerCase() + if (userAgent.indexOf('google') !== -1) { + return metrics.inc('crawler.google') + } else if (userAgent.indexOf('facebook') !== -1) { + return metrics.inc('crawler.facebook') + } else if (userAgent.indexOf('bing') !== -1) { + return metrics.inc('crawler.bing') + } + } + } +} diff --git a/services/web/app/src/infrastructure/Csrf.js b/services/web/app/src/infrastructure/Csrf.js new file mode 100644 index 0000000000..8b7099d8e1 --- /dev/null +++ b/services/web/app/src/infrastructure/Csrf.js @@ -0,0 +1,96 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Csrf +const csurf = require('csurf') +const csrf = csurf() + +// Wrapper for `csurf` middleware that provides a list of routes that can be excluded from csrf checks. +// +// Include with `Csrf = require('./Csrf')` +// +// Add the middleware to the router with: +// myRouter.csrf = new Csrf() +// myRouter.use webRouter.csrf.middleware +// When building routes, specify a route to exclude from csrf checks with: +// myRouter.csrf.disableDefaultCsrfProtection "/path" "METHOD" +// +// To validate the csrf token in a request to ensure that it's valid, you can use `validateRequest`, which takes a +// request object and calls a callback with either true or false. + +module.exports = Csrf = class Csrf { + constructor() { + this.middleware = this.middleware.bind(this) + this.excluded_routes = {} + } + + disableDefaultCsrfProtection(route, method) { + if (!this.excluded_routes[route]) { + this.excluded_routes[route] = {} + } + return (this.excluded_routes[route][method] = 1) + } + + middleware(req, res, next) { + // We want to call the middleware for all routes, even if excluded, because csurf sets up a csrfToken() method on + // the request, to get a new csrf token for any rendered forms. For excluded routes we'll then ignore a 'bad csrf + // token' error from csurf and continue on... + + // check whether the request method is excluded for the specified route + if ( + (this.excluded_routes[req.path] != null + ? this.excluded_routes[req.path][req.method] + : undefined) === 1 + ) { + // ignore the error if it's due to a bad csrf token, and continue + return csrf(req, res, err => { + if (err && err.code !== 'EBADCSRFTOKEN') { + return next(err) + } else { + return next() + } + }) + } else { + return csrf(req, res, next) + } + } + + static validateRequest(req, cb) { + // run a dummy csrf check to see if it returns an error + if (cb == null) { + cb = function(valid) {} + } + return csrf(req, null, err => cb(err == null)) + } + + static validateToken(token, session, cb) { + if (cb == null) { + cb = function(valid) {} + } + if (token == null) { + return cb(false) + } + // run a dummy csrf check to see if it returns an error + // use this to simulate a csrf check regardless of req method, headers &c. + const req = { + body: { + _csrf: token + }, + headers: {}, + method: 'POST', + session + } + return Csrf.validateRequest(req, cb) + } +} diff --git a/services/web/app/src/infrastructure/Events.js b/services/web/app/src/infrastructure/Events.js new file mode 100644 index 0000000000..25c2261c1a --- /dev/null +++ b/services/web/app/src/infrastructure/Events.js @@ -0,0 +1,4 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +const events = require('events') +module.exports = new events.EventEmitter() diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js new file mode 100644 index 0000000000..e9e147b29e --- /dev/null +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -0,0 +1,563 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + new-cap, + no-new-require, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let path +const logger = require('logger-sharelatex') +const fs = require('fs') +const crypto = require('crypto') +const Settings = require('settings-sharelatex') +const SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters') +const querystring = require('querystring') +const SystemMessageManager = require('../Features/SystemMessages/SystemMessageManager') +const AuthenticationController = require('../Features/Authentication/AuthenticationController') +const _ = require('underscore') +const async = require('async') +let Modules = require('./Modules') +const Url = require('url') +const PackageVersions = require('./PackageVersions') +const htmlEncoder = new require('node-html-encoder').Encoder('numerical') +const hashedFiles = {} +const Path = require('path') +const Features = require('./Features') +Modules = require('./Modules') +const moment = require('moment') + +const jsPath = Settings.useMinifiedJs ? '/minjs/' : '/js/' + +const ace = PackageVersions.lib('ace') +const pdfjs = PackageVersions.lib('pdfjs') +const fineuploader = PackageVersions.lib('fineuploader') + +const getFileContent = function(filePath) { + filePath = Path.join(__dirname, '../../../', `public${filePath}`) + const exists = fs.existsSync(filePath) + if (exists) { + const content = fs.readFileSync(filePath, 'UTF-8') + return content + } else { + logger.log({ filePath }, 'file does not exist for hashing') + return '' + } +} + +const pathList = [ + `${jsPath}libs/require.js`, + `${jsPath}ide.js`, + `${jsPath}main.js`, + `${jsPath}libraries.js`, + '/stylesheets/style.css', + '/stylesheets/light-style.css', + '/stylesheets/ieee-style.css', + '/stylesheets/sl-style.css' +].concat(Modules.moduleAssetFiles(jsPath)) + +if (!Settings.useMinifiedJs) { + logger.log('not using minified JS, not hashing static files') +} else { + logger.log('Generating file hashes...') + for (path of Array.from(pathList)) { + const content = getFileContent(path) + const hash = crypto + .createHash('md5') + .update(content) + .digest('hex') + + const splitPath = path.split('/') + const filenameSplit = splitPath.pop().split('.') + filenameSplit.splice(filenameSplit.length - 1, 0, hash) + splitPath.push(filenameSplit.join('.')) + + const hashPath = splitPath.join('/') + hashedFiles[path] = hashPath + + const fsHashPath = Path.join(__dirname, '../../../', `public${hashPath}`) + fs.writeFileSync(fsHashPath, content) + + logger.log('Finished hashing static content') + } +} + +const cdnAvailable = + __guard__(Settings.cdn != null ? Settings.cdn.web : undefined, x => x.host) != + null +const darkCdnAvailable = + __guard__( + Settings.cdn != null ? Settings.cdn.web : undefined, + x1 => x1.darkHost + ) != null + +module.exports = function(app, webRouter, privateApiRouter, publicApiRouter) { + webRouter.use(function(req, res, next) { + res.locals.session = req.session + return next() + }) + + const addSetContentDisposition = function(req, res, next) { + res.setContentDisposition = function(type, opts) { + const directives = (() => { + const result = [] + for (let k in opts) { + const v = opts[k] + result.push(`${k}=\"${encodeURIComponent(v)}\"`) + } + return result + })() + const contentDispositionValue = `${type}; ${directives.join('; ')}` + return res.setHeader('Content-Disposition', contentDispositionValue) + } + return next() + } + webRouter.use(addSetContentDisposition) + privateApiRouter.use(addSetContentDisposition) + publicApiRouter.use(addSetContentDisposition) + + webRouter.use(function(req, res, next) { + req.externalAuthenticationSystemUsed = + Features.externalAuthenticationSystemUsed + res.locals.externalAuthenticationSystemUsed = + Features.externalAuthenticationSystemUsed + req.hasFeature = res.locals.hasFeature = Features.hasFeature + res.locals.userIsFromOLv1 = user => + (user.overleaf != null ? user.overleaf.id : undefined) != null + res.locals.userIsFromSL = user => + (user.overleaf != null ? user.overleaf.id : undefined) == null + return next() + }) + + webRouter.use(function(req, res, next) { + let staticFilesBase + const cdnBlocked = req.query.nocdn === 'true' || req.session.cdnBlocked + const user_id = AuthenticationController.getLoggedInUserId(req) + + if (cdnBlocked && req.session.cdnBlocked == null) { + logger.log( + { user_id, ip: req != null ? req.ip : undefined }, + 'cdnBlocked for user, not using it and turning it off for future requets' + ) + req.session.cdnBlocked = true + } + + const isDark = + __guard__( + __guard__(req.headers != null ? req.headers.host : undefined, x3 => + x3.slice(0, 7) + ), + x2 => x2.toLowerCase().indexOf('dark') + ) !== -1 + const isSmoke = + __guard__( + __guard__(req.headers != null ? req.headers.host : undefined, x5 => + x5.slice(0, 5) + ), + x4 => x4.toLowerCase() + ) === 'smoke' + const isLive = !isDark && !isSmoke + + if (cdnAvailable && isLive && !cdnBlocked) { + staticFilesBase = __guard__( + Settings.cdn != null ? Settings.cdn.web : undefined, + x6 => x6.host + ) + } else if (darkCdnAvailable && isDark) { + staticFilesBase = __guard__( + Settings.cdn != null ? Settings.cdn.web : undefined, + x7 => x7.darkHost + ) + } else { + staticFilesBase = '' + } + + res.locals.jsPath = jsPath + res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath) + res.locals.lib = PackageVersions.lib + + res.locals.moment = moment + + res.locals.buildJsPath = function(jsFile, opts) { + if (opts == null) { + opts = {} + } + path = Path.join(jsPath, jsFile) + + if (opts.hashedPath && hashedFiles[path] != null) { + path = hashedFiles[path] + } + + if (opts.qs == null) { + opts.qs = {} + } + + if (opts.cdn !== false) { + path = Url.resolve(staticFilesBase, path) + } + + const qs = querystring.stringify(opts.qs) + + if (opts.removeExtension === true) { + path = path.slice(0, -3) + } + + if (qs != null && qs.length > 0) { + path = path + '?' + qs + } + return path + } + + res.locals.buildWebpackPath = function(jsFile, opts) { + if (opts == null) { + opts = {} + } + if (Settings.webpack != null && !Settings.useMinifiedJs) { + path = Path.join(jsPath, jsFile) + if (opts.removeExtension === true) { + path = path.slice(0, -3) + } + return `${Settings.webpack.url}/public${path}` + } else { + return res.locals.buildJsPath(jsFile, opts) + } + } + + const IEEE_BRAND_ID = 15 + res.locals.isIEEE = brandVariation => + (brandVariation != null ? brandVariation.brand_id : undefined) === + IEEE_BRAND_ID + + const _buildCssFileName = themeModifier => + `/${Settings.brandPrefix}${themeModifier || ''}style.css` + + res.locals.getCssThemeModifier = function(userSettings, brandVariation) { + // Themes only exist in OL v2 + let themeModifier + if (Settings.overleaf != null) { + // The IEEE theme takes precedence over the user personal setting, i.e. a user with + // a theme setting of "light" will still get the IEE theme in IEEE branded projects. + if (res.locals.isIEEE(brandVariation)) { + themeModifier = 'ieee-' + } else if ( + (userSettings != null ? userSettings.overallTheme : undefined) != null + ) { + themeModifier = userSettings.overallTheme + } + } + return themeModifier + } + + res.locals.buildCssPath = function(themeModifier, buildOpts) { + const cssFileName = _buildCssFileName(themeModifier) + path = Path.join('/stylesheets/', cssFileName) + if ( + (buildOpts != null ? buildOpts.hashedPath : undefined) && + hashedFiles[path] != null + ) { + const hashedPath = hashedFiles[path] + return Url.resolve(staticFilesBase, hashedPath) + } + return Url.resolve(staticFilesBase, path) + } + + res.locals.buildImgPath = function(imgFile) { + path = Path.join('/img/', imgFile) + return Url.resolve(staticFilesBase, path) + } + + res.locals.mathJaxPath = res.locals.buildJsPath('libs/mathjax/MathJax.js', { + cdn: false, + qs: { config: 'TeX-AMS_HTML,Safe' } + }) + + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.settings = Settings + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.translate = function(key, vars, htmlEncode) { + if (vars == null) { + vars = {} + } + if (htmlEncode == null) { + htmlEncode = false + } + vars.appName = Settings.appName + const str = req.i18n.translate(key, vars) + if (htmlEncode) { + return htmlEncoder.htmlEncode(str) + } else { + return str + } + } + // Don't include the query string parameters, otherwise Google + // treats ?nocdn=true as the canonical version + res.locals.currentUrl = Url.parse(req.originalUrl).pathname + res.locals.capitalize = function(string) { + if (string.length === 0) { + return '' + } + return string.charAt(0).toUpperCase() + string.slice(1) + } + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.getSiteHost = () => + Settings.siteUrl.substring(Settings.siteUrl.indexOf('//') + 2) + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.getUserEmail = function() { + const user = AuthenticationController.getSessionUser(req) + const email = (user != null ? user.email : undefined) || '' + return email + } + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.StringHelper = require('../Features/Helpers/StringHelper') + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.formatProjectPublicAccessLevel = function(privilegeLevel) { + const formatedPrivileges = { + private: 'Private', + readOnly: 'Public: Read Only', + readAndWrite: 'Public: Read and Write' + } + return formatedPrivileges[privilegeLevel] || 'Private' + } + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.buildReferalUrl = function(referal_medium) { + let url = Settings.siteUrl + const currentUser = AuthenticationController.getSessionUser(req) + if ( + currentUser != null && + (currentUser != null ? currentUser.referal_id : undefined) != null + ) { + url += `?r=${currentUser.referal_id}&rm=${referal_medium}&rs=b` // Referal source = bonus + } + return url + } + res.locals.getReferalId = function() { + const currentUser = AuthenticationController.getSessionUser(req) + if ( + currentUser != null && + (currentUser != null ? currentUser.referal_id : undefined) != null + ) { + return currentUser.referal_id + } + } + res.locals.getReferalTagLine = function() { + const tagLines = [ + 'Roar!', + 'Shout about us!', + 'Please recommend us', + 'Tell the world!', + 'Thanks for using ShareLaTeX' + ] + return tagLines[Math.floor(Math.random() * tagLines.length)] + } + res.locals.getRedirAsQueryString = function() { + if (req.query.redir != null) { + return `?${querystring.stringify({ redir: req.query.redir })}` + } + return '' + } + + res.locals.getLoggedInUserId = () => + AuthenticationController.getLoggedInUserId(req) + res.locals.isUserLoggedIn = () => + AuthenticationController.isUserLoggedIn(req) + res.locals.getSessionUser = () => + AuthenticationController.getSessionUser(req) + + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.csrfToken = req != null ? req.csrfToken() : undefined + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.getReqQueryParam = field => + req.query != null ? req.query[field] : undefined + return next() + }) + + webRouter.use(function(req, res, next) { + res.locals.formatPrice = SubscriptionFormatters.formatPrice + return next() + }) + + webRouter.use(function(req, res, next) { + const currentUser = AuthenticationController.getSessionUser(req) + if (currentUser != null) { + res.locals.user = { + email: currentUser.email, + first_name: currentUser.first_name, + last_name: currentUser.last_name + } + if (req.session.justRegistered) { + res.locals.justRegistered = true + delete req.session.justRegistered + } + if (req.session.justLoggedIn) { + res.locals.justLoggedIn = true + delete req.session.justLoggedIn + } + } + res.locals.gaToken = __guard__( + Settings.analytics != null ? Settings.analytics.ga : undefined, + x2 => x2.token + ) + res.locals.tenderUrl = Settings.tenderUrl + res.locals.sentrySrc = + Settings.sentry != null ? Settings.sentry.src : undefined + res.locals.sentryPublicDSN = + Settings.sentry != null ? Settings.sentry.publicDSN : undefined + return next() + }) + + webRouter.use(function(req, res, next) { + if (req.query != null && req.query.scribtex_path != null) { + res.locals.lookingForScribtex = true + res.locals.scribtexPath = req.query.scribtex_path + } + return next() + }) + + webRouter.use(function(req, res, next) { + // Clone the nav settings so they can be modified for each request + res.locals.nav = {} + for (let key in Settings.nav) { + const value = Settings.nav[key] + res.locals.nav[key] = _.clone(Settings.nav[key]) + } + res.locals.templates = Settings.templateLinks + if (res.locals.nav.header) { + console.error( + {}, + 'The `nav.header` setting is no longer supported, use `nav.header_extras` instead' + ) + } + return next() + }) + + webRouter.use((req, res, next) => + SystemMessageManager.getMessages(function(error, messages) { + if (messages == null) { + messages = [] + } + res.locals.systemMessages = messages + return next() + }) + ) + + webRouter.use(function(req, res, next) { + res.locals.query = req.query + return next() + }) + + webRouter.use(function(req, res, next) { + const subdomain = _.find( + Settings.i18n.subdomainLang, + subdomain => subdomain.lngCode === req.showUserOtherLng && !subdomain.hide + ) + res.locals.recomendSubdomain = subdomain + res.locals.currentLngCode = req.lng + return next() + }) + + webRouter.use(function(req, res, next) { + if (Settings.reloadModuleViewsOnEachRequest) { + Modules.loadViewIncludes() + } + res.locals.moduleIncludes = Modules.moduleIncludes + res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable + return next() + }) + + webRouter.use(function(req, res, next) { + const isSl = Settings.brandPrefix === 'sl-' + res.locals.uiConfig = { + defaultResizerSizeOpen: isSl ? 24 : 7, + defaultResizerSizeClosed: isSl ? 24 : 7, + eastResizerCursor: isSl ? null : 'ew-resize', + westResizerCursor: isSl ? null : 'ew-resize', + chatResizerSizeOpen: isSl ? 12 : 7, + chatResizerSizeClosed: 0, + chatMessageBorderSaturation: isSl ? '70%' : '85%', + chatMessageBorderLightness: isSl ? '70%' : '40%', + chatMessageBgSaturation: isSl ? '60%' : '85%', + chatMessageBgLightness: isSl ? '97%' : '40%', + defaultFontFamily: isSl ? 'monaco' : 'lucida', + defaultLineHeight: isSl ? 'compact' : 'normal', + renderAnnouncements: isSl + } + return next() + }) + + webRouter.use(function(req, res, next) { + // TODO + if (Settings.overleaf != null) { + res.locals.overallThemes = [ + { + name: 'Default', + val: '', + path: res.locals.buildCssPath(null, { hashedPath: true }) + }, + { + name: 'Light', + val: 'light-', + path: res.locals.buildCssPath('light-', { hashedPath: true }) + } + ] + } + return next() + }) + + return webRouter.use(function(req, res, next) { + res.locals.ExposedSettings = { + isOverleaf: Settings.overleaf != null, + appName: Settings.appName, + siteUrl: Settings.siteUrl, + recaptchaSiteKeyV3: + Settings.recaptcha != null ? Settings.recaptcha.siteKeyV3 : undefined, + recaptchaDisabled: + Settings.recaptcha != null ? Settings.recaptcha.disabled : undefined + } + return next() + }) +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/infrastructure/Features.js b/services/web/app/src/infrastructure/Features.js new file mode 100644 index 0000000000..940194bd05 --- /dev/null +++ b/services/web/app/src/infrastructure/Features.js @@ -0,0 +1,76 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Features +const Settings = require('settings-sharelatex') + +module.exports = Features = { + externalAuthenticationSystemUsed() { + return ( + Settings.ldap != null || + Settings.saml != null || + (Settings.overleaf != null ? Settings.overleaf.oauth : undefined) != null + ) + }, + + hasFeature(feature) { + switch (feature) { + case 'homepage': + return Settings.enableHomepage + case 'registration': + return ( + !Features.externalAuthenticationSystemUsed() || + Settings.overleaf != null + ) + case 'github-sync': + return Settings.enableGithubSync + case 'git-bridge': + return Settings.enableGitBridge + case 'v1-return-message': + return ( + Settings.accountMerge != null && + Settings.overleaf != null && + !Settings.forceImportToV2 + ) + case 'custom-togglers': + return Settings.overleaf != null + case 'oauth': + return Settings.oauth != null + case 'publish-templates': + return true + case 'view-templates': + return Settings.overleaf == null + case 'affiliations': + return ( + __guard__( + __guard__( + Settings != null ? Settings.apis : undefined, + x1 => x1.v1 + ), + x => x.url + ) != null + ) + case 'redirect-sl': + return Settings.redirectToV2 != null + case 'force-import-to-v2': + return Settings.forceImportToV2 + default: + throw new Error(`unknown feature: ${feature}`) + } + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/infrastructure/FileWriter.js b/services/web/app/src/infrastructure/FileWriter.js new file mode 100644 index 0000000000..043a64fade --- /dev/null +++ b/services/web/app/src/infrastructure/FileWriter.js @@ -0,0 +1,118 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FileWriter +const fs = require('fs') +const logger = require('logger-sharelatex') +const uuid = require('uuid') +const _ = require('underscore') +const Settings = require('settings-sharelatex') +const request = require('request') + +module.exports = FileWriter = { + ensureDumpFolderExists(callback) { + if (callback == null) { + callback = function(error) {} + } + return fs.mkdir(Settings.path.dumpFolder, function(error) { + if (error != null && error.code !== 'EEXIST') { + // Ignore error about already existing + return callback(error) + } + return callback(null) + }) + }, + + writeLinesToDisk(identifier, lines, callback) { + if (callback == null) { + callback = function(error, fsPath) {} + } + return FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback) + }, + + writeContentToDisk(identifier, content, callback) { + if (callback == null) { + callback = function(error, fsPath) {} + } + callback = _.once(callback) + const fsPath = `${Settings.path.dumpFolder}/${identifier}_${uuid.v4()}` + return FileWriter.ensureDumpFolderExists(function(error) { + if (error != null) { + return callback(error) + } + return fs.writeFile(fsPath, content, function(error) { + if (error != null) { + return callback(error) + } + return callback(null, fsPath) + }) + }) + }, + + writeStreamToDisk(identifier, stream, callback) { + if (callback == null) { + callback = function(error, fsPath) {} + } + callback = _.once(callback) + const fsPath = `${Settings.path.dumpFolder}/${identifier}_${uuid.v4()}` + + stream.pause() + return FileWriter.ensureDumpFolderExists(function(error) { + if (error != null) { + return callback(error) + } + stream.resume() + + const writeStream = fs.createWriteStream(fsPath) + stream.pipe(writeStream) + + stream.on('error', function(err) { + logger.err( + { err, identifier, fsPath }, + '[writeStreamToDisk] something went wrong with incoming stream' + ) + return callback(err) + }) + writeStream.on('error', function(err) { + logger.err( + { err, identifier, fsPath }, + '[writeStreamToDisk] something went wrong with writing to disk' + ) + return callback(err) + }) + return writeStream.on('finish', function() { + logger.log( + { identifier, fsPath }, + '[writeStreamToDisk] write stream finished' + ) + return callback(null, fsPath) + }) + }) + }, + + writeUrlToDisk(identifier, url, callback) { + if (callback == null) { + callback = function(error, fsPath) {} + } + callback = _.once(callback) + const stream = request.get(url) + return stream.on('response', function(response) { + if (response.statusCode >= 200 && response.statusCode < 300) { + return FileWriter.writeStreamToDisk(identifier, stream, callback) + } else { + const err = new Error(`bad response from url: ${response.statusCode}`) + logger.err({ err, identifier, url }, err.message) + return callback(err) + } + }) + } +} diff --git a/services/web/app/src/infrastructure/GeoIpLookup.js b/services/web/app/src/infrastructure/GeoIpLookup.js new file mode 100644 index 0000000000..55d9312fdc --- /dev/null +++ b/services/web/app/src/infrastructure/GeoIpLookup.js @@ -0,0 +1,109 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let GeoIpLookup +const request = require('request') +const settings = require('settings-sharelatex') +const _ = require('underscore') +const logger = require('logger-sharelatex') +const URL = require('url') + +const currencyMappings = { + GB: 'GBP', + US: 'USD', + CH: 'CHF', + NZ: 'NZD', + AU: 'AUD', + DK: 'DKK', + NO: 'NOK', + CA: 'CAD', + SE: 'SEK' +} + +// Countries which would likely prefer Euro's +const EuroCountries = [ + 'AT', + 'BE', + 'BG', + 'HR', + 'CY', + 'CZ', + 'EE', + 'FI', + 'FR', + 'DE', + 'EL', + 'HU', + 'IE', + 'IT', + 'LV', + 'LT', + 'LU', + 'MT', + 'NL', + 'PL', + 'PT', + 'RO', + 'SK', + 'SI', + 'ES' +] + +_.each(EuroCountries, country => (currencyMappings[country] = 'EUR')) + +module.exports = GeoIpLookup = { + getDetails(ip, callback) { + if (ip == null) { + const e = new Error('no ip passed') + return callback(e) + } + ip = ip.trim().split(' ')[0] + const opts = { + url: URL.resolve(settings.apis.geoIpLookup.url, ip), + timeout: 1000, + json: true + } + logger.log({ ip, opts }, 'getting geo ip details') + return request.get(opts, function(err, res, ipDetails) { + if (err != null) { + logger.err({ err, ip }, 'error getting ip details') + } + return callback(err, ipDetails) + }) + }, + + getCurrencyCode(ip, callback) { + return GeoIpLookup.getDetails(ip, function(err, ipDetails) { + if (err != null || ipDetails == null) { + logger.err( + { err, ip }, + 'problem getting currencyCode for ip, defaulting to USD' + ) + return callback(null, 'USD') + } + const countryCode = __guard__( + ipDetails != null ? ipDetails.country_code : undefined, + x => x.toUpperCase() + ) + const currencyCode = currencyMappings[countryCode] || 'USD' + logger.log({ ip, currencyCode, ipDetails }, 'got currencyCode for ip') + return callback(err, currencyCode, countryCode) + }) + } +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/infrastructure/Keys.js b/services/web/app/src/infrastructure/Keys.js new file mode 100644 index 0000000000..fceae8054f --- /dev/null +++ b/services/web/app/src/infrastructure/Keys.js @@ -0,0 +1,8 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +module.exports = { + queue: { + web_to_tpds_http_requests: 'web_to_tpds_http_requests', + tpds_to_web_http_requests: 'tpds_to_web_http_requests' + } +} diff --git a/services/web/app/src/infrastructure/LockManager.js b/services/web/app/src/infrastructure/LockManager.js new file mode 100644 index 0000000000..4342a2ea31 --- /dev/null +++ b/services/web/app/src/infrastructure/LockManager.js @@ -0,0 +1,217 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LockManager +const metrics = require('metrics-sharelatex') +const Settings = require('settings-sharelatex') +const RedisWrapper = require('./RedisWrapper') +const rclient = RedisWrapper.client('lock') +const logger = require('logger-sharelatex') +const os = require('os') +const crypto = require('crypto') +const async = require('async') + +const HOST = os.hostname() +const PID = process.pid +const RND = crypto.randomBytes(4).toString('hex') +let COUNT = 0 + +const LOCK_QUEUES = new Map() // queue lock requests for each name/id so they get the lock on a first-come first-served basis + +module.exports = LockManager = { + LOCK_TEST_INTERVAL: 50, // 50ms between each test of the lock + MAX_TEST_INTERVAL: 1000, // back off to 1s between each test of the lock + MAX_LOCK_WAIT_TIME: 10000, // 10s maximum time to spend trying to get the lock + REDIS_LOCK_EXPIRY: 30, // seconds. Time until lock auto expires in redis + SLOW_EXECUTION_THRESHOLD: 5000, // 5s, if execution takes longer than this then log + + // Use a signed lock value as described in + // http://redis.io/topics/distlock#correct-implementation-with-a-single-instance + // to prevent accidental unlocking by multiple processes + randomLock() { + const time = Date.now() + return `locked:host=${HOST}:pid=${PID}:random=${RND}:time=${time}:count=${COUNT++}` + }, + + unlockScript: + 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end', + + runWithLock(namespace, id, runner, callback) { + // runner must be a function accepting a callback, e.g. runner = (cb) -> + + // This error is defined here so we get a useful stacktrace + if (callback == null) { + callback = function(error) {} + } + const slowExecutionError = new Error('slow execution during lock') + + const timer = new metrics.Timer(`lock.${namespace}`) + const key = `lock:web:${namespace}:${id}` + return LockManager._getLock(key, namespace, function(error, lockValue) { + if (error != null) { + return callback(error) + } + + // The lock can expire in redis but the process carry on. This setTimout call + // is designed to log if this happens. + const countIfExceededLockTimeout = function() { + metrics.inc(`lock.${namespace}.exceeded_lock_timeout`) + return logger.log('exceeded lock timeout', { + namespace, + id, + slowExecutionError + }) + } + const exceededLockTimeout = setTimeout( + countIfExceededLockTimeout, + LockManager.REDIS_LOCK_EXPIRY * 1000 + ) + + return runner((error1, ...values) => + LockManager._releaseLock(key, lockValue, function(error2) { + clearTimeout(exceededLockTimeout) + + const timeTaken = new Date() - timer.start + if (timeTaken > LockManager.SLOW_EXECUTION_THRESHOLD) { + logger.log('slow execution during lock', { + namespace, + id, + timeTaken, + slowExecutionError + }) + } + + timer.done() + error = error1 || error2 + if (error != null) { + return callback(error) + } + return callback(null, ...Array.from(values)) + }) + ) + }) + }, + + _tryLock(key, namespace, callback) { + if (callback == null) { + callback = function(err, isFree, lockValue) {} + } + const lockValue = LockManager.randomLock() + return rclient.set( + key, + lockValue, + 'EX', + LockManager.REDIS_LOCK_EXPIRY, + 'NX', + function(err, gotLock) { + if (err != null) { + return callback(err) + } + if (gotLock === 'OK') { + metrics.inc(`lock.${namespace}.try.success`) + return callback(err, true, lockValue) + } else { + metrics.inc(`lock.${namespace}.try.failed`) + logger.log({ key, redis_response: gotLock }, 'lock is locked') + return callback(err, false) + } + } + ) + }, + + // it's sufficient to serialize within a process because that is where the parallel operations occur + _getLock(key, namespace, callback) { + // this is what we need to do for each lock we want to request + if (callback == null) { + callback = function(error, lockValue) {} + } + const task = next => + LockManager._getLockByPolling(key, namespace, function(error, lockValue) { + // tell the queue to start trying to get the next lock (if any) + next() + // we have got a lock result, so we can continue with our own execution + return callback(error, lockValue) + }) + // create a queue for this key if needed + const queueName = `${key}:${namespace}` + let queue = LOCK_QUEUES.get(queueName) + if (queue == null) { + const handler = (fn, cb) => fn(cb) + // set up a new queue for this key + queue = async.queue(handler, 1) + queue.push(task) + // remove the queue object when queue is empty + queue.drain = () => LOCK_QUEUES.delete(queueName) + // store the queue in our global map + return LOCK_QUEUES.set(queueName, queue) + } else { + // queue the request to get the lock + return queue.push(task) + } + }, + + _getLockByPolling(key, namespace, callback) { + let attempt + if (callback == null) { + callback = function(error, lockValue) {} + } + const startTime = Date.now() + const testInterval = LockManager.LOCK_TEST_INTERVAL + let attempts = 0 + return (attempt = function() { + if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) { + metrics.inc(`lock.${namespace}.get.failed`) + return callback(new Error('Timeout')) + } + + attempts += 1 + return LockManager._tryLock(key, namespace, function( + error, + gotLock, + lockValue + ) { + if (error != null) { + return callback(error) + } + if (gotLock) { + metrics.gauge(`lock.${namespace}.get.success.tries`, attempts) + return callback(null, lockValue) + } else { + return setTimeout(attempt, testInterval) + } + }) + })() + }, + + _releaseLock(key, lockValue, callback) { + return rclient.eval(LockManager.unlockScript, 1, key, lockValue, function( + err, + result + ) { + if (err != null) { + return callback(err) + } else if (result != null && result !== 1) { + // successful unlock should release exactly one key + logger.error( + { key, lockValue, redis_err: err, redis_result: result }, + 'unlocking error' + ) + metrics.inc('unlock-error') + return callback(new Error('tried to release timed out lock')) + } else { + return callback(null, result) + } + }) + } +} diff --git a/services/web/app/src/infrastructure/LoggerSerializers.js b/services/web/app/src/infrastructure/LoggerSerializers.js new file mode 100644 index 0000000000..5ca2b80c5e --- /dev/null +++ b/services/web/app/src/infrastructure/LoggerSerializers.js @@ -0,0 +1,57 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +module.exports = { + user(user) { + if (user == null) { + return null + } + if (user._id == null) { + user = { _id: user } + } + return { + id: user._id, + email: user.email, + first_name: user.name, + last_name: user.name + } + }, + + project(project) { + if (project == null) { + return null + } + if (project._id == null) { + project = { _id: project } + } + return { + id: project._id, + name: project.name + } + }, + + docs(docs) { + if ((docs != null ? docs.map : undefined) == null) { + return + } + return docs.map(doc => ({ + path: doc.path, + id: doc.doc + })) + }, + + files(files) { + if ((files != null ? files.map : undefined) == null) { + return + } + return files.map(file => ({ + path: file.path, + id: file.file + })) + } +} diff --git a/services/web/app/src/infrastructure/Modules.js b/services/web/app/src/infrastructure/Modules.js new file mode 100644 index 0000000000..7ff2f510d2 --- /dev/null +++ b/services/web/app/src/infrastructure/Modules.js @@ -0,0 +1,200 @@ +/* eslint-disable + camelcase, + max-len, + no-path-concat, + no-unused-vars, + one-var, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS201: Simplify complex destructure assignments + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Modules +const fs = require('fs') +const Path = require('path') +const pug = require('pug') +const async = require('async') + +const MODULE_BASE_PATH = Path.resolve(__dirname + '/../../../modules') + +module.exports = Modules = { + modules: [], + loadModules() { + for (let moduleName of Array.from(fs.readdirSync(MODULE_BASE_PATH))) { + if (fs.existsSync(Path.join(MODULE_BASE_PATH, moduleName, 'index.js'))) { + const loadedModule = require(Path.join( + MODULE_BASE_PATH, + moduleName, + 'index' + )) + loadedModule.name = moduleName + this.modules.push(loadedModule) + } + } + return Modules.attachHooks() + }, + + applyRouter(webRouter, privateApiRouter, publicApiRouter) { + return Array.from(this.modules).map(module => + __guardMethod__(module.router, 'apply', o => + o.apply(webRouter, privateApiRouter, publicApiRouter) + ) + ) + }, + + applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) { + return (() => { + const result = [] + for (let module of Array.from(this.modules)) { + if (module.nonCsrfRouter != null) { + module.nonCsrfRouter.apply( + webRouter, + privateApiRouter, + publicApiRouter + ) + } + result.push( + __guardMethod__(module.router, 'applyNonCsrfRouter', o => + o.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) + ) + ) + } + return result + })() + }, + + viewIncludes: {}, + loadViewIncludes(app) { + this.viewIncludes = {} + return Array.from(this.modules).map(module => + (() => { + const result = [] + const object = module.viewIncludes || {} + for (let view in object) { + const partial = object[view] + if (!this.viewIncludes[view]) { + this.viewIncludes[view] = [] + } + const filePath = Path.join( + MODULE_BASE_PATH, + module.name, + 'app/views', + partial + '.pug' + ) + result.push( + this.viewIncludes[view].push( + pug.compileFile(filePath, { doctype: 'html' }) + ) + ) + } + return result + })() + ) + }, + + moduleIncludes(view, locals) { + const compiledPartials = Modules.viewIncludes[view] || [] + let html = '' + for (let compiledPartial of Array.from(compiledPartials)) { + const d = new Date() + html += compiledPartial(locals) + } + return html + }, + + moduleIncludesAvailable(view) { + return (Modules.viewIncludes[view] || []).length > 0 + }, + + moduleAssetFiles(pathPrefix) { + const assetFiles = [] + for (let module of Array.from(this.modules)) { + for (let assetFile of Array.from(module.assetFiles || [])) { + assetFiles.push(`${pathPrefix}${assetFile}`) + } + } + return assetFiles + }, + + linkedFileAgentsIncludes() { + const agents = {} + for (let module of Array.from(this.modules)) { + for (let name in module.linkedFileAgents) { + const agentFunction = module.linkedFileAgents[name] + agents[name] = agentFunction() + } + } + return agents + }, + + attachHooks() { + return (() => { + const result = [] + for (var module of Array.from(this.modules)) { + if (module.hooks != null) { + result.push( + (() => { + const result1 = [] + for (let hook in module.hooks) { + const method = module.hooks[hook] + result1.push(Modules.hooks.attach(hook, method)) + } + return result1 + })() + ) + } else { + result.push(undefined) + } + } + return result + })() + }, + + hooks: { + _hooks: {}, + attach(name, method) { + if (this._hooks[name] == null) { + this._hooks[name] = [] + } + return this._hooks[name].push(method) + }, + + fire(name, ...rest) { + const adjustedLength = Math.max(rest.length, 1), + args = rest.slice(0, adjustedLength - 1), + callback = rest[adjustedLength - 1] + const methods = this._hooks[name] || [] + const call_methods = methods.map(method => cb => + method(...Array.from(args), cb) + ) + return async.series(call_methods, function(error, results) { + if (error != null) { + return callback(error) + } + return callback(null, results) + }) + } + } +} + +Modules.loadModules() + +function __guardMethod__(obj, methodName, transform) { + if ( + typeof obj !== 'undefined' && + obj !== null && + typeof obj[methodName] === 'function' + ) { + return transform(obj, methodName) + } else { + return undefined + } +} diff --git a/services/web/app/src/infrastructure/Mongoose.js b/services/web/app/src/infrastructure/Mongoose.js new file mode 100644 index 0000000000..466a8297a5 --- /dev/null +++ b/services/web/app/src/infrastructure/Mongoose.js @@ -0,0 +1,41 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') + +mongoose.connect( + Settings.mongo.url, + { + server: { poolSize: 10 }, + config: { autoIndex: false } + } +) + +mongoose.connection.on('connected', () => + logger.log({ url: Settings.mongo.url }, 'mongoose default connection open') +) + +mongoose.connection.on('error', err => + logger.err({ err }, 'mongoose error on default connection') +) + +mongoose.connection.on('disconnected', () => + logger.log('mongoose default connection disconnected') +) + +if (process.env.MONGOOSE_DEBUG) { + mongoose.set('debug', (collectionName, method, query, doc) => + logger.debug('mongoose debug', { collectionName, method, query, doc }) + ) +} + +module.exports = mongoose diff --git a/services/web/app/src/infrastructure/PackageVersions.js b/services/web/app/src/infrastructure/PackageVersions.js new file mode 100644 index 0000000000..150a7071c0 --- /dev/null +++ b/services/web/app/src/infrastructure/PackageVersions.js @@ -0,0 +1,25 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const version = { + pdfjs: '2.0.943', + moment: '2.9.0', + ace: '1.4.4', // Upgrade instructions: https://github.com/overleaf/write_latex/wiki/Upgrading-Ace + fineuploader: '5.15.4' +} + +module.exports = { + version, + + lib(name) { + if (version[name] != null) { + return `${name}-${version[name]}` + } else { + return `${name}` + } + } +} diff --git a/services/web/app/src/infrastructure/ProxyManager.js b/services/web/app/src/infrastructure/ProxyManager.js new file mode 100644 index 0000000000..c6a190a40e --- /dev/null +++ b/services/web/app/src/infrastructure/ProxyManager.js @@ -0,0 +1,90 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProxyManager +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const request = require('request') +const URL = require('url') + +module.exports = ProxyManager = { + apply(publicApiRouter) { + return (() => { + const result = [] + for (var proxyUrl in settings.proxyUrls) { + const target = settings.proxyUrls[proxyUrl] + result.push( + (function(target) { + const method = + (target.options != null ? target.options.method : undefined) || + 'get' + return publicApiRouter[method]( + proxyUrl, + ProxyManager.createProxy(target) + ) + })(target) + ) + } + return result + })() + }, + + createProxy(target) { + return function(req, res, next) { + const targetUrl = makeTargetUrl(target, req) + logger.log({ targetUrl, reqUrl: req.url }, 'proxying url') + + const options = { url: targetUrl } + if (req.headers != null ? req.headers.cookie : undefined) { + options.headers = { Cookie: req.headers.cookie } + } + if ((target != null ? target.options : undefined) != null) { + Object.assign(options, target.options) + } + if (['post', 'put'].includes(options.method)) { + options.form = req.body + } + const upstream = request(options) + upstream.on('error', error => + logger.error({ err: error }, 'error in ProxyManager') + ) + + // TODO: better handling of status code + // see https://github.com/overleaf/write_latex/wiki/Streams-and-pipes-in-Node.js + return upstream.pipe(res) + } + } +} + +// make a URL from a proxy target. +// if the query is specified, set/replace the target's query with the given query +var makeTargetUrl = function(target, req) { + const targetUrl = URL.parse(parseSettingUrl(target, req)) + if (req.query != null && Object.keys(req.query).length > 0) { + targetUrl.query = req.query + targetUrl.search = null // clear `search` as it takes precedence over `query` + } + return targetUrl.format() +} + +var parseSettingUrl = function(target, { params }) { + let path + if (typeof target === 'string') { + return target + } + if (typeof target.path === 'function') { + path = target.path(params) + } else { + ;({ path } = target) + } + return `${target.baseUrl}${path || ''}` +} diff --git a/services/web/app/src/infrastructure/RandomLogging.js b/services/web/app/src/infrastructure/RandomLogging.js new file mode 100644 index 0000000000..5cea4bc56a --- /dev/null +++ b/services/web/app/src/infrastructure/RandomLogging.js @@ -0,0 +1,21 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let trackOpenSockets +const _ = require('underscore') +const metrics = require('metrics-sharelatex') +;(trackOpenSockets = function() { + metrics.gauge( + 'http.open-sockets', + _.size(require('http').globalAgent.sockets.length), + 0.5 + ) + return setTimeout(trackOpenSockets, 1000) +})() diff --git a/services/web/app/src/infrastructure/RateLimiter.js b/services/web/app/src/infrastructure/RateLimiter.js new file mode 100644 index 0000000000..74138d6e33 --- /dev/null +++ b/services/web/app/src/infrastructure/RateLimiter.js @@ -0,0 +1,53 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RateLimiter +const settings = require('settings-sharelatex') +const Metrics = require('metrics-sharelatex') +const RedisWrapper = require('./RedisWrapper') +const rclient = RedisWrapper.client('ratelimiter') +const RollingRateLimiter = require('rolling-rate-limiter') + +module.exports = RateLimiter = { + addCount(opts, callback) { + if (callback == null) { + callback = function(err, shouldProcess) {} + } + const namespace = `RateLimit:${opts.endpointName}:` + const k = `{${opts.subjectName}}` + const limiter = RollingRateLimiter({ + redis: rclient, + namespace, + interval: opts.timeInterval * 1000, + maxInInterval: opts.throttle + }) + return limiter(k, function(err, timeLeft, actionsLeft) { + if (err != null) { + return callback(err) + } + const allowed = timeLeft === 0 + if (!allowed) { + Metrics.inc(`rate-limit-hit.${opts.endpointName}`, 1, { + path: opts.endpointName + }) + } + return callback(null, allowed) + }) + }, + + clearRateLimit(endpointName, subject, callback) { + // same as the key which will be built by RollingRateLimiter (namespace+k) + const keyName = `RateLimit:${endpointName}:{${subject}}` + return rclient.del(keyName, callback) + } +} diff --git a/services/web/app/src/infrastructure/RedirectManager.js b/services/web/app/src/infrastructure/RedirectManager.js new file mode 100644 index 0000000000..f5aadfb589 --- /dev/null +++ b/services/web/app/src/infrastructure/RedirectManager.js @@ -0,0 +1,94 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RedirectManager +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const URL = require('url') +const querystring = require('querystring') + +module.exports = RedirectManager = { + apply(webRouter) { + return (() => { + const result = [] + for (var redirectUrl in settings.redirects) { + var target = settings.redirects[redirectUrl] + result.push( + Array.from(target.methods || ['get']).map(method => + webRouter[method]( + redirectUrl, + RedirectManager.createRedirect(target) + ) + ) + ) + } + return result + })() + }, + + createRedirect(target) { + return function(req, res, next) { + let url + if ( + (req.headers != null ? req.headers['x-skip-redirects'] : undefined) != + null + ) { + return next() + } + let code = 302 + if (typeof target === 'string') { + url = target + } else { + if (req.method !== 'GET') { + code = 307 + } + + if (typeof target.url === 'function') { + url = target.url(req.params) + if (!url) { + return next() + } + } else { + ;({ url } = target) + } + + // Special handling for redirecting to v1, to ensure that query params + // are encoded + if (target.authWithV1) { + url = `/sign_in_to_v1?${querystring.stringify({ + return_to: url + getQueryString(req) + })}` + return res.redirect(code, url) + } + + if (target.baseUrl != null) { + url = `${target.baseUrl}${url}` + } + } + return res.redirect(code, url + getQueryString(req)) + } + } +} + +// Naively get the query params string. Stringifying the req.query object may +// have differences between Express and Rails, so safer to just pass the raw +// string +var getQueryString = function(req) { + const { search } = URL.parse(req.url) + if (search) { + return search + } else { + return '' + } +} diff --git a/services/web/app/src/infrastructure/RedisWrapper.js b/services/web/app/src/infrastructure/RedisWrapper.js new file mode 100644 index 0000000000..07b288e06b --- /dev/null +++ b/services/web/app/src/infrastructure/RedisWrapper.js @@ -0,0 +1,23 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +let Redis +const Settings = require('settings-sharelatex') +const redis = require('redis-sharelatex') + +// A per-feature interface to Redis, +// looks up the feature in `settings.redis` +// and returns an appropriate client. +// Necessary because we don't want to migrate web over +// to redis-cluster all at once. +module.exports = Redis = { + // feature = 'websessions' | 'ratelimiter' | ... + client(feature) { + const redisFeatureSettings = Settings.redis[feature] || Settings.redis.web + const rclient = redis.createClient(redisFeatureSettings) + return rclient + } +} diff --git a/services/web/app/src/infrastructure/Server.js b/services/web/app/src/infrastructure/Server.js new file mode 100644 index 0000000000..ad7da30096 --- /dev/null +++ b/services/web/app/src/infrastructure/Server.js @@ -0,0 +1,255 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-path-concat, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let staticCacheAge +const Path = require('path') +const express = require('express') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const crawlerLogger = require('./CrawlerLogger') +const expressLocals = require('./ExpressLocals') +const Router = require('../router') +const helmet = require('helmet') +const UserSessionsRedis = require('../Features/User/UserSessionsRedis') +const Csrf = require('./Csrf') + +const sessionsRedisClient = UserSessionsRedis.client() + +const session = require('express-session') +const RedisStore = require('connect-redis')(session) +const bodyParser = require('body-parser') +const methodOverride = require('method-override') +const cookieParser = require('cookie-parser') +const bearerToken = require('express-bearer-token') + +// Init the session store +const sessionStore = new RedisStore({ client: sessionsRedisClient }) + +const passport = require('passport') +const LocalStrategy = require('passport-local').Strategy + +const Mongoose = require('./Mongoose') + +const oneDayInMilliseconds = 86400000 +const ReferalConnect = require('../Features/Referal/ReferalConnect') +const RedirectManager = require('./RedirectManager') +const ProxyManager = require('./ProxyManager') +const translations = require('translations-sharelatex').setup(Settings.i18n) +const Modules = require('./Modules') + +const ErrorController = require('../Features/Errors/ErrorController') +const UserSessionsManager = require('../Features/User/UserSessionsManager') +const AuthenticationController = require('../Features/Authentication/AuthenticationController') + +if (metrics.event_loop != null) { + metrics.event_loop.monitor(logger) +} + +if (Settings.cacheStaticAssets) { + staticCacheAge = oneDayInMilliseconds * 365 +} else { + staticCacheAge = 0 +} + +const app = express() + +const webRouter = express.Router() +const privateApiRouter = express.Router() +const publicApiRouter = express.Router() + +if (Settings.behindProxy) { + app.enable('trust proxy') +} + +webRouter.use( + express.static(__dirname + '/../../../public', { maxAge: staticCacheAge }) +) +app.set('views', __dirname + '/../../views') +app.set('view engine', 'pug') +Modules.loadViewIncludes(app) + +app.use(bodyParser.urlencoded({ extended: true, limit: '2mb' })) +// Make sure we can process twice the max doc length, to allow for +// - the doc content +// - text ranges spanning the whole doc +// Also allow some overhead for JSON encoding +app.use(bodyParser.json({ limit: 2 * Settings.max_doc_length + 64 * 1024 })) // 64kb overhead +app.use(methodOverride()) +app.use(bearerToken()) + +app.use(metrics.http.monitor(logger)) +RedirectManager.apply(webRouter) +ProxyManager.apply(publicApiRouter) + +webRouter.use(cookieParser(Settings.security.sessionSecret)) +webRouter.use( + session({ + resave: false, + saveUninitialized: false, + secret: Settings.security.sessionSecret, + proxy: Settings.behindProxy, + cookie: { + domain: Settings.cookieDomain, + maxAge: Settings.cookieSessionLength, + secure: Settings.secureCookie + }, + store: sessionStore, + key: Settings.cookieName, + rolling: true + }) +) + +// passport +webRouter.use(passport.initialize()) +webRouter.use(passport.session()) + +passport.use( + new LocalStrategy( + { + passReqToCallback: true, + usernameField: 'email', + passwordField: 'password' + }, + AuthenticationController.doPassportLogin + ) +) +passport.serializeUser(AuthenticationController.serializeUser) +passport.deserializeUser(AuthenticationController.deserializeUser) + +Modules.hooks.fire('passportSetup', passport, function(err) { + if (err != null) { + return logger.err({ err }, 'error setting up passport in modules') + } +}) + +Modules.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) + +webRouter.csrf = new Csrf() +webRouter.use(webRouter.csrf.middleware) +webRouter.use(translations.expressMiddlewear) +webRouter.use(translations.setLangBasedOnDomainMiddlewear) + +// Measure expiry from last request, not last login +webRouter.use(function(req, res, next) { + req.session.touch() + if (AuthenticationController.isUserLoggedIn(req)) { + UserSessionsManager.touch( + AuthenticationController.getSessionUser(req), + function(err) {} + ) + } + return next() +}) + +webRouter.use(ReferalConnect.use) +expressLocals(app, webRouter, privateApiRouter, publicApiRouter) + +if (app.get('env') === 'production') { + logger.info('Production Enviroment') + app.enable('view cache') +} + +app.use(function(req, res, next) { + metrics.inc('http-request') + crawlerLogger.log(req) + return next() +}) + +webRouter.use(function(req, res, next) { + if (Settings.siteIsOpen) { + return next() + } else { + res.status(503) + return res.render('general/closed', { title: 'maintenance' }) + } +}) + +webRouter.use(function(req, res, next) { + if (Settings.editorIsOpen) { + return next() + } else if (req.url.indexOf('/admin') === 0) { + return next() + } else { + res.status(503) + return res.render('general/closed', { title: 'maintenance' }) + } +}) + +// add security headers using Helmet +webRouter.use(function(req, res, next) { + const isLoggedIn = AuthenticationController.isUserLoggedIn(req) + const isProjectPage = !!req.path.match('^/project/[a-f0-9]{24}$') + + return helmet({ + // note that more headers are added by default + dnsPrefetchControl: false, + referrerPolicy: { policy: 'origin-when-cross-origin' }, + noCache: isLoggedIn || isProjectPage, + noSniff: false, + hsts: false, + frameguard: false + })(req, res, next) +}) + +const profiler = require('v8-profiler-node8') +privateApiRouter.get('/profile', function(req, res) { + const time = parseInt(req.query.time || '1000') + profiler.startProfiling('test') + return setTimeout(function() { + const profile = profiler.stopProfiling('test') + return res.json(profile) + }, time) +}) + +privateApiRouter.get('/heapdump', (req, res) => + require('heapdump').writeSnapshot( + `/tmp/${Date.now()}.web.heapsnapshot`, + (err, filename) => res.send(filename) + ) +) + +logger.info('creating HTTP server'.yellow) +const server = require('http').createServer(app) + +// provide settings for separate web and api processes +// if enableApiRouter and enableWebRouter are not defined they default +// to true. +const notDefined = x => x == null +const enableApiRouter = + Settings.web != null ? Settings.web.enableApiRouter : undefined +if (enableApiRouter || notDefined(enableApiRouter)) { + logger.info('providing api router') + app.use(privateApiRouter) + app.use(ErrorController.handleApiError) +} + +const enableWebRouter = + Settings.web != null ? Settings.web.enableWebRouter : undefined +if (enableWebRouter || notDefined(enableWebRouter)) { + logger.info('providing web router') + app.use(publicApiRouter) // public API goes with web router for public access + app.use(ErrorController.handleApiError) + app.use(webRouter) + app.use(ErrorController.handleError) +} + +metrics.injectMetricsRoute(webRouter) + +const router = new Router(webRouter, privateApiRouter, publicApiRouter) + +module.exports = { + app, + server +} diff --git a/services/web/app/src/infrastructure/Sixpack.js b/services/web/app/src/infrastructure/Sixpack.js new file mode 100644 index 0000000000..fe54a0fb53 --- /dev/null +++ b/services/web/app/src/infrastructure/Sixpack.js @@ -0,0 +1,156 @@ +/* eslint-disable + camelcase, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let sixpack +const settings = require('settings-sharelatex') +const request = require('request') +const logger = require('logger-sharelatex') + +const timeout = process.env.NODE_ENV === 'production' ? 500 : 5000 +logger.log(`using timeout of ${timeout}ms for sixpack server calls`) + +const generate_client_id = () => + // from http://stackoverflow.com/questions/105034 + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) + +const _request_uri = function(endpoint, params) { + const query_string = [] + const e = encodeURIComponent + for (let key in params) { + if (params.hasOwnProperty(key)) { + let vals = params[key] + if (Object.prototype.toString.call(vals) !== '[object Array]') { + vals = [vals] + } + let i = 0 + while (i < vals.length) { + query_string.push(e(key) + '=' + e(vals[i])) + i += 1 + } + } + } + if (query_string.length) { + endpoint += `?${query_string.join('&')}` + } + return endpoint +} + +const _request = function(uri, params, callback) { + const opts = { + uri: _request_uri(uri, params), + json: true, + timeout + } + return request.get(opts, (err, res, body) => callback(err, body)) +} + +module.exports = sixpack = { + client(user_id) { + const client = new sixpack.Session(user_id, settings.apis.sixpack.url) + return client + }, + + Session(client_id, base_url, ip_address, user_agent) { + this.client_id = client_id || sixpack.generate_client_id() + this.base_url = base_url || sixpack.base_url + + return { + participate: (experiment_name, alternatives, force, callback) => { + if (typeof force === 'function') { + callback = force + force = null + } + if (!/^[a-z0-9][a-z0-9\-_ ]*$/.test(experiment_name)) { + return callback(new Error('Bad experiment_name')) + } + if (alternatives.length < 2) { + return callback(new Error('Must specify at least 2 alternatives')) + } + let i = 0 + while (i < alternatives.length) { + if (!/^[a-z0-9][a-z0-9\-_ ]*$/.test(alternatives[i])) { + return callback( + new Error(`Bad alternative name: ${alternatives[i]}`) + ) + } + i += 1 + } + const params = { + client_id: this.client_id, + experiment: experiment_name, + alternatives + } + + if (force !== null && _in_array(alternatives, force)) { + return callback(null, { + status: 'ok', + alternative: { + name: force + }, + experiment: { + version: 0, + name: experiment_name + }, + client_id: this.client_id + }) + } + + return _request(this.base_url + '/participate', params, function( + err, + res + ) { + if (err != null) { + res = { + status: 'failed', + error: err, + alternative: { + name: alternatives[0] + } + } + } + return callback(null, res) + }) + }, + + convert: (experiment_name, callback) => { + if (!/^[a-z0-9][a-z0-9\-_ ]*$/.test(experiment_name)) { + return callback(new Error('Bad experiment_name')) + } + const params = { + client_id: this.client_id, + experiment: experiment_name + } + if (this.ip_address) { + params.ip_address = this.ip_address + } + if (this.user_agent) { + params.user_agent = this.user_agent + } + return _request(this.base_url + '/convert', params, function(err, res) { + if (err != null) { + res = { + status: 'failed', + error: err + } + } + return callback(null, res) + }) + } + } + } +} diff --git a/services/web/app/src/infrastructure/mongojs.js b/services/web/app/src/infrastructure/mongojs.js new file mode 100644 index 0000000000..1a9301de24 --- /dev/null +++ b/services/web/app/src/infrastructure/mongojs.js @@ -0,0 +1,19 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const Settings = require('settings-sharelatex') +const mongojs = require('mongojs') +const db = mongojs(Settings.mongo.url, [ + 'projects', + 'users', + 'userstubs', + 'tokens', + 'docSnapshots', + 'projectHistoryFailures' +]) +module.exports = { + db, + ObjectId: mongojs.ObjectId +} diff --git a/services/web/app/src/models/DeletedProject.js b/services/web/app/src/models/DeletedProject.js new file mode 100644 index 0000000000..d94d09f4a2 --- /dev/null +++ b/services/web/app/src/models/DeletedProject.js @@ -0,0 +1,33 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') +const { ProjectSchema } = require('./Project.js') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const DeleterDataSchema = new Schema({ + deleterId: { type: ObjectId, ref: 'User' }, + deleterIpAddress: { type: String }, + deletedAt: { type: Date } +}) + +const DeletedProjectSchema = new Schema( + { + deleterData: [DeleterDataSchema], + project: [ProjectSchema] + }, + { collection: 'deletedProjects' } +) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const DeletedProject = conn.model('DeletedProject', DeletedProjectSchema) + +mongoose.model('DeletedProject', DeletedProjectSchema) +exports.DeletedProject = DeletedProject +exports.DeletedProjectSchema = DeletedProjectSchema diff --git a/services/web/app/src/models/Doc.js b/services/web/app/src/models/Doc.js new file mode 100644 index 0000000000..93aa99c2cc --- /dev/null +++ b/services/web/app/src/models/Doc.js @@ -0,0 +1,18 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const DocSchema = new Schema({ + name: { type: String, default: 'new doc' } +}) + +mongoose.model('Doc', DocSchema) +exports.Doc = mongoose.model('Doc') +exports.DocSchema = DocSchema diff --git a/services/web/app/src/models/File.js b/services/web/app/src/models/File.js new file mode 100644 index 0000000000..f089f13cf7 --- /dev/null +++ b/services/web/app/src/models/File.js @@ -0,0 +1,32 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const FileSchema = new Schema({ + name: { + type: String, + default: '' + }, + created: { + type: Date, + default() { + return new Date() + } + }, + rev: { type: Number, default: 0 }, + linkedFileData: { type: Schema.Types.Mixed }, + hash: { + type: String + } +}) + +mongoose.model('File', FileSchema) +exports.File = mongoose.model('File') +exports.FileSchema = FileSchema diff --git a/services/web/app/src/models/Folder.js b/services/web/app/src/models/Folder.js new file mode 100644 index 0000000000..efd0608ec3 --- /dev/null +++ b/services/web/app/src/models/Folder.js @@ -0,0 +1,26 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') +const { DocSchema } = require('./Doc') +const { FileSchema } = require('./File') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const FolderSchema = new Schema({ + name: { type: String, default: 'new folder' } +}) + +FolderSchema.add({ + docs: [DocSchema], + fileRefs: [FileSchema], + folders: [FolderSchema] +}) + +mongoose.model('Folder', FolderSchema) +exports.Folder = mongoose.model('Folder') +exports.FolderSchema = FolderSchema diff --git a/services/web/app/src/models/Institution.js b/services/web/app/src/models/Institution.js new file mode 100644 index 0000000000..7425989d62 --- /dev/null +++ b/services/web/app/src/models/Institution.js @@ -0,0 +1,62 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const mongoose = require('mongoose') +const { Schema } = mongoose +const { ObjectId } = Schema +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const request = require('request') + +const InstitutionSchema = new Schema({ + v1Id: { type: Number, required: true }, + managerIds: [{ type: ObjectId, ref: 'User' }], + metricsEmail: { + optedOutUserIds: [{ type: ObjectId, ref: 'User' }], + lastSent: { type: Date } + } +}) + +// fetch institution's data from v1 API. Errors are ignored +InstitutionSchema.method('fetchV1Data', function(callback) { + if (callback == null) { + callback = function(error, institution) {} + } + const url = `${settings.apis.v1.url}/universities/list/${this.v1Id}` + return request.get(url, (error, response, body) => { + let parsedBody + try { + parsedBody = JSON.parse(body) + } catch (error1) { + // log error and carry on without v1 data + error = error1 + logger.err( + { model: 'Institution', v1Id: this.v1Id, error }, + '[fetchV1DataError]' + ) + } + this.name = parsedBody != null ? parsedBody.name : undefined + this.countryCode = parsedBody != null ? parsedBody.country_code : undefined + this.departments = parsedBody != null ? parsedBody.departments : undefined + this.portalSlug = parsedBody != null ? parsedBody.portal_slug : undefined + return callback(null, this) + }) +}) + +const conn = mongoose.createConnection(settings.mongo.url, { + server: { poolSize: settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const Institution = conn.model('Institution', InstitutionSchema) +exports.Institution = Institution +exports.InstitutionSchema = InstitutionSchema diff --git a/services/web/app/src/models/OauthAccessToken.js b/services/web/app/src/models/OauthAccessToken.js new file mode 100644 index 0000000000..61011aa3cb --- /dev/null +++ b/services/web/app/src/models/OauthAccessToken.js @@ -0,0 +1,33 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const OauthAccessTokenSchema = new Schema( + { + accessToken: String, + accessTokenExpiresAt: Date, + oauthApplication_id: { type: ObjectId, ref: 'OauthApplication' }, + refreshToken: String, + refreshTokenExpiresAt: Date, + scope: String, + user_id: { type: ObjectId, ref: 'User' } + }, + { + collection: 'oauthAccessTokens' + } +) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const OauthAccessToken = conn.model('OauthAccessToken', OauthAccessTokenSchema) + +mongoose.model('OauthAccessToken', OauthAccessTokenSchema) +exports.OauthAccessToken = OauthAccessToken +exports.OauthAccessTokenSchema = OauthAccessTokenSchema diff --git a/services/web/app/src/models/OauthApplication.js b/services/web/app/src/models/OauthApplication.js new file mode 100644 index 0000000000..5c728f6ae1 --- /dev/null +++ b/services/web/app/src/models/OauthApplication.js @@ -0,0 +1,35 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const OauthApplicationSchema = new Schema( + { + id: String, + clientSecret: String, + grants: [String], + name: String, + redirectUris: [String], + scopes: [String] + }, + { + collection: 'oauthApplications' + } +) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const OauthApplication = conn.model('OauthApplication', OauthApplicationSchema) + +mongoose.model('OauthApplication', OauthApplicationSchema) +exports.OauthApplication = OauthApplication +exports.OauthApplicationSchema = OauthApplicationSchema diff --git a/services/web/app/src/models/OauthAuthorizationCode.js b/services/web/app/src/models/OauthAuthorizationCode.js new file mode 100644 index 0000000000..003606c3ef --- /dev/null +++ b/services/web/app/src/models/OauthAuthorizationCode.js @@ -0,0 +1,38 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const OauthAuthorizationCodeSchema = new Schema( + { + authorizationCode: String, + expiresAt: Date, + oauthApplication_id: { type: ObjectId, ref: 'OauthApplication' }, + redirectUri: String, + scope: String, + user_id: { type: ObjectId, ref: 'User' } + }, + { + collection: 'oauthAuthorizationCodes' + } +) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const OauthAuthorizationCode = conn.model( + 'OauthAuthorizationCode', + OauthAuthorizationCodeSchema +) + +mongoose.model('OauthAuthorizationCode', OauthAuthorizationCodeSchema) +exports.OauthAuthorizationCode = OauthAuthorizationCode +exports.OauthAuthorizationCodeSchema = OauthAuthorizationCodeSchema diff --git a/services/web/app/src/models/Project.js b/services/web/app/src/models/Project.js new file mode 100644 index 0000000000..85924a6f5e --- /dev/null +++ b/services/web/app/src/models/Project.js @@ -0,0 +1,157 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') +const _ = require('underscore') +const { FolderSchema } = require('./Folder.js') +const logger = require('logger-sharelatex') +const concreteObjectId = require('mongoose').Types.ObjectId +const Errors = require('../Features/Errors/Errors') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const DeletedDocSchema = new Schema({ + name: String, + deletedAt: { type: Date } +}) + +const DeletedFileSchema = new Schema({ + name: String, + created: { + type: Date + }, + linkedFileData: { type: Schema.Types.Mixed }, + hash: { + type: String + }, + deletedAt: { type: Date } +}) + +const ProjectSchema = new Schema({ + name: { type: String, default: 'new project' }, + lastUpdated: { + type: Date, + default() { + return new Date() + } + }, + lastUpdatedBy: { type: ObjectId, ref: 'User' }, + lastOpened: { type: Date }, + active: { type: Boolean, default: true }, + owner_ref: { type: ObjectId, ref: 'User' }, + collaberator_refs: [{ type: ObjectId, ref: 'User' }], + readOnly_refs: [{ type: ObjectId, ref: 'User' }], + rootDoc_id: { type: ObjectId }, + rootFolder: [FolderSchema], + version: { type: Number }, // incremented for every change in the project structure (folders and filenames) + publicAccesLevel: { type: String, default: 'private' }, + compiler: { type: String, default: 'pdflatex' }, + spellCheckLanguage: { type: String, default: 'en' }, + deletedByExternalDataSource: { type: Boolean, default: false }, + description: { type: String, default: '' }, + archived: { type: Boolean }, + deletedDocs: [DeletedDocSchema], + deletedFiles: [DeletedFileSchema], + imageName: { type: String }, + brandVariationId: { type: String }, + track_changes: { type: Object }, + tokens: { + readOnly: { + type: String, + index: { + unique: true, + partialFilterExpression: { 'tokens.readOnly': { $exists: true } } + } + }, + readAndWrite: { + type: String, + index: { + unique: true, + partialFilterExpression: { 'tokens.readAndWrite': { $exists: true } } + } + }, + readAndWritePrefix: { + type: String, + index: { + unique: true, + partialFilterExpression: { + 'tokens.readAndWritePrefix': { $exists: true } + } + } + } + }, + tokenAccessReadOnly_refs: [{ type: ObjectId, ref: 'User' }], + tokenAccessReadAndWrite_refs: [{ type: ObjectId, ref: 'User' }], + fromV1TemplateId: { type: Number }, + fromV1TemplateVersionId: { type: Number }, + overleaf: { + id: { type: Number }, + imported_at_ver_id: { type: Number }, + token: { type: String }, + read_token: { type: String }, + history: { + id: { type: Number }, + display: { type: Boolean }, + upgradedAt: { type: Date } + } + }, + collabratecUsers: [ + { + user_id: { type: ObjectId, ref: 'User' }, + collabratec_document_id: { type: String }, + collabratec_privategroup_id: { type: String }, + added_at: { + type: Date, + default() { + return new Date() + } + } + } + ] +}) + +ProjectSchema.statics.getProject = function(project_or_id, fields, callback) { + if (project_or_id._id != null) { + return callback(null, project_or_id) + } else { + try { + concreteObjectId(project_or_id.toString()) + } catch (e) { + return callback(new Errors.NotFoundError(e.message)) + } + return this.findById(project_or_id, fields, callback) + } +} + +var applyToAllFilesRecursivly = (ProjectSchema.statics.applyToAllFilesRecursivly = function( + folder, + fun +) { + _.each(folder.fileRefs, file => fun(file)) + return _.each(folder.folders, folder => + applyToAllFilesRecursivly(folder, fun) + ) +}) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const Project = conn.model('Project', ProjectSchema) + +mongoose.model('Project', ProjectSchema) +exports.Project = Project +exports.ProjectSchema = ProjectSchema diff --git a/services/web/app/src/models/ProjectInvite.js b/services/web/app/src/models/ProjectInvite.js new file mode 100644 index 0000000000..03a8d1b873 --- /dev/null +++ b/services/web/app/src/models/ProjectInvite.js @@ -0,0 +1,49 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30 + +const ExpiryDate = function() { + const timestamp = new Date() + timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS) + return timestamp +} + +const ProjectInviteSchema = new Schema( + { + email: String, + token: String, + sendingUserId: ObjectId, + projectId: ObjectId, + privileges: String, + createdAt: { type: Date, default: Date.now }, + expires: { + type: Date, + default: ExpiryDate, + index: { expireAfterSeconds: 10 } + } + }, + { + collection: 'projectInvites' + } +) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema) + +mongoose.model('ProjectInvite', ProjectInviteSchema) +exports.ProjectInvite = ProjectInvite +exports.ProjectInviteSchema = ProjectInviteSchema +exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS diff --git a/services/web/app/src/models/Publisher.js b/services/web/app/src/models/Publisher.js new file mode 100644 index 0000000000..8efa962f63 --- /dev/null +++ b/services/web/app/src/models/Publisher.js @@ -0,0 +1,67 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const mongoose = require('mongoose') +const { Schema } = mongoose +const { ObjectId } = Schema +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const request = require('request') + +const PublisherSchema = new Schema({ + slug: { type: String, required: true }, + managerIds: [{ type: ObjectId, ref: 'User' }] +}) + +// fetch publisher's (brand on v1) data from v1 API. Errors are ignored +PublisherSchema.method('fetchV1Data', function(callback) { + if (callback == null) { + callback = function(error, publisher) {} + } + return request( + { + baseUrl: settings.apis.v1.url, + url: `/api/v2/brands/${this.slug}`, + method: 'GET', + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + sendImmediately: true + } + }, + (error, response, body) => { + let parsedBody + try { + parsedBody = JSON.parse(body) + } catch (error1) { + // log error and carry on without v1 data + error = error1 + logger.err( + { model: 'Publisher', slug: this.slug, error }, + '[fetchV1DataError]' + ) + } + this.name = parsedBody != null ? parsedBody.name : undefined + this.partner = parsedBody != null ? parsedBody.partner : undefined + return callback(null, this) + } + ) +}) + +const conn = mongoose.createConnection(settings.mongo.url, { + server: { poolSize: settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const Publisher = conn.model('Publisher', PublisherSchema) +exports.Publisher = Publisher +exports.PublisherSchema = PublisherSchema diff --git a/services/web/app/src/models/Subscription.js b/services/web/app/src/models/Subscription.js new file mode 100644 index 0000000000..8d477c6945 --- /dev/null +++ b/services/web/app/src/models/Subscription.js @@ -0,0 +1,70 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') +const { TeamInviteSchema } = require('./TeamInvite') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const SubscriptionSchema = new Schema({ + admin_id: { + type: ObjectId, + ref: 'User', + index: { unique: true, dropDups: true } + }, + manager_ids: [{ type: ObjectId, ref: 'User' }], + member_ids: [{ type: ObjectId, ref: 'User' }], + invited_emails: [String], + teamInvites: [TeamInviteSchema], + recurlySubscription_id: String, + teamName: { type: String }, + teamNotice: { type: String }, + planCode: { type: String }, + groupPlan: { type: Boolean, default: false }, + membersLimit: { type: Number, default: 0 }, + customAccount: Boolean, + overleaf: { + id: { + type: Number, + index: { + unique: true, + partialFilterExpression: { 'overleaf.id': { $exists: true } } + } + } + } +}) + +SubscriptionSchema.statics.findAndModify = function(query, update, callback) { + const self = this + return this.update(query, update, () => self.findOne(query, callback)) +} + +// Subscriptions have no v1 data to fetch +SubscriptionSchema.method('fetchV1Data', function(callback) { + if (callback == null) { + callback = function(error, subscription) {} + } + return callback(null, this) +}) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const Subscription = conn.model('Subscription', SubscriptionSchema) + +mongoose.model('Subscription', SubscriptionSchema) +exports.Subscription = Subscription +exports.SubscriptionSchema = SubscriptionSchema diff --git a/services/web/app/src/models/SystemMessage.js b/services/web/app/src/models/SystemMessage.js new file mode 100644 index 0000000000..5b61f96a8f --- /dev/null +++ b/services/web/app/src/models/SystemMessage.js @@ -0,0 +1,21 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const SystemMessageSchema = new Schema({ + content: { type: String, default: '' } +}) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +exports.SystemMessage = conn.model('SystemMessage', SystemMessageSchema) diff --git a/services/web/app/src/models/TeamInvite.js b/services/web/app/src/models/TeamInvite.js new file mode 100644 index 0000000000..d0a1ab7cd0 --- /dev/null +++ b/services/web/app/src/models/TeamInvite.js @@ -0,0 +1,21 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const mongoose = require('mongoose') +const Settings = require('settings-sharelatex') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const TeamInviteSchema = new Schema({ + email: { type: String, required: true }, + token: { type: String }, + inviterName: { type: String }, + sentAt: { type: Date } +}) + +mongoose.model('TeamInvite', TeamInviteSchema) +exports.TeamInvite = mongoose.model('TeamInvite') +exports.TeamInviteSchema = TeamInviteSchema diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js new file mode 100644 index 0000000000..988e020385 --- /dev/null +++ b/services/web/app/src/models/User.js @@ -0,0 +1,132 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const { Project } = require('./Project') +const Settings = require('settings-sharelatex') +const _ = require('underscore') +const mongoose = require('mongoose') +const uuid = require('uuid') +const { Schema } = mongoose +const { ObjectId } = Schema + +// See https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/574698#574698 +const MAX_EMAIL_LENGTH = 254 + +const UserSchema = new Schema({ + email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH }, + emails: [ + { + email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH }, + reversedHostname: { type: String, default: '' }, + createdAt: { + type: Date, + default() { + return new Date() + } + }, + confirmedAt: { type: Date } + } + ], + first_name: { type: String, default: '' }, + last_name: { type: String, default: '' }, + role: { type: String, default: '' }, + institution: { type: String, default: '' }, + hashedPassword: String, + isAdmin: { type: Boolean, default: false }, + staffAccess: { + publisherMetrics: { type: Boolean, default: false }, + publisherManagement: { type: Boolean, default: false }, + institutionMetrics: { type: Boolean, default: false }, + institutionManagement: { type: Boolean, default: false }, + groupMetrics: { type: Boolean, default: false }, + groupManagement: { type: Boolean, default: false } + }, + signUpDate: { + type: Date, + default() { + return new Date() + } + }, + lastLoggedIn: { type: Date }, + lastLoginIp: { type: String, default: '' }, + loginCount: { type: Number, default: 0 }, + holdingAccount: { type: Boolean, default: false }, + ace: { + mode: { type: String, default: 'none' }, + theme: { type: String, default: 'textmate' }, + overallTheme: { type: String, default: '' }, + fontSize: { type: Number, default: '12' }, + autoComplete: { type: Boolean, default: true }, + autoPairDelimiters: { type: Boolean, default: true }, + spellCheckLanguage: { type: String, default: 'en' }, + pdfViewer: { type: String, default: 'pdfjs' }, + syntaxValidation: { type: Boolean }, + fontFamily: { type: String }, + lineHeight: { type: String } + }, + features: { + collaborators: { + type: Number, + default: Settings.defaultFeatures.collaborators + }, + versioning: { type: Boolean, default: Settings.defaultFeatures.versioning }, + dropbox: { type: Boolean, default: Settings.defaultFeatures.dropbox }, + github: { type: Boolean, default: Settings.defaultFeatures.github }, + gitBridge: { type: Boolean, default: Settings.defaultFeatures.gitBridge }, + compileTimeout: { + type: Number, + default: Settings.defaultFeatures.compileTimeout + }, + compileGroup: { + type: String, + default: Settings.defaultFeatures.compileGroup + }, + templates: { type: Boolean, default: Settings.defaultFeatures.templates }, + references: { type: Boolean, default: Settings.defaultFeatures.references }, + trackChanges: { + type: Boolean, + default: Settings.defaultFeatures.trackChanges + }, + mendeley: { type: Boolean, default: Settings.defaultFeatures.mendeley }, + zotero: { type: Boolean, default: Settings.defaultFeatures.zotero }, + referencesSearch: { + type: Boolean, + default: Settings.defaultFeatures.referencesSearch + } + }, + must_reconfirm: { type: Boolean, default: false }, + referal_id: { + type: String, + default() { + return uuid.v4().split('-')[0] + } + }, + refered_users: [{ type: ObjectId, ref: 'User' }], + refered_user_count: { type: Number, default: 0 }, + refProviders: { + mendeley: Boolean, // coerce the refProviders values to Booleans + zotero: Boolean + }, + betaProgram: { type: Boolean, default: false }, + overleaf: { + id: { type: Number }, + accessToken: { type: String }, + refreshToken: { type: String } + }, + awareOfV2: { type: Boolean, default: false }, + thirdPartyIdentifiers: { type: Array, default: [] }, + migratedAt: { type: Date } +}) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const User = conn.model('User', UserSchema) + +const model = mongoose.model('User', UserSchema) +exports.User = User diff --git a/services/web/app/src/models/UserStub.js b/services/web/app/src/models/UserStub.js new file mode 100644 index 0000000000..89e5f1bea5 --- /dev/null +++ b/services/web/app/src/models/UserStub.js @@ -0,0 +1,28 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +const Settings = require('settings-sharelatex') +const mongoose = require('mongoose') +const { Schema } = mongoose +const { ObjectId } = Schema + +const UserStubSchema = new Schema({ + email: { type: String, default: '' }, + first_name: { type: String, default: '' }, + last_name: { type: String, default: '' }, + overleaf: { id: { type: Number } }, + thirdPartyIdentifiers: { type: Array, default: [] }, + confirmed_at: Date +}) + +const conn = mongoose.createConnection(Settings.mongo.url, { + server: { poolSize: Settings.mongo.poolSize || 10 }, + config: { autoIndex: false } +}) + +const UserStub = conn.model('UserStub', UserStubSchema) + +const model = mongoose.model('UserStub', UserStubSchema) +exports.UserStub = UserStub diff --git a/services/web/app/src/router.js b/services/web/app/src/router.js new file mode 100644 index 0000000000..09e36119c4 --- /dev/null +++ b/services/web/app/src/router.js @@ -0,0 +1,1039 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Router +const AdminController = require('./Features/ServerAdmin/AdminController') +const ErrorController = require('./Features/Errors/ErrorController') +const ProjectController = require('./Features/Project/ProjectController') +const ProjectApiController = require('./Features/Project/ProjectApiController') +const SpellingController = require('./Features/Spelling/SpellingController') +const EditorController = require('./Features/Editor/EditorController') +const EditorRouter = require('./Features/Editor/EditorRouter') +const Settings = require('settings-sharelatex') +const TpdsController = require('./Features/ThirdPartyDataStore/TpdsController') +const SubscriptionRouter = require('./Features/Subscription/SubscriptionRouter') +const UploadsRouter = require('./Features/Uploads/UploadsRouter') +const metrics = require('metrics-sharelatex') +const ReferalController = require('./Features/Referal/ReferalController') +const AuthenticationController = require('./Features/Authentication/AuthenticationController') +const TagsController = require('./Features/Tags/TagsController') +const NotificationsController = require('./Features/Notifications/NotificationsController') +const CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter') +const UserInfoController = require('./Features/User/UserInfoController') +const UserController = require('./Features/User/UserController') +const UserEmailsController = require('./Features/User/UserEmailsController') +const UserPagesController = require('./Features/User/UserPagesController') +const DocumentController = require('./Features/Documents/DocumentController') +const CompileManager = require('./Features/Compile/CompileManager') +const CompileController = require('./Features/Compile/CompileController') +const ClsiCookieManager = require('./Features/Compile/ClsiCookieManager')( + Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined +) +const HealthCheckController = require('./Features/HealthCheck/HealthCheckController') +const ProjectDownloadsController = require('./Features/Downloads/ProjectDownloadsController') +const FileStoreController = require('./Features/FileStore/FileStoreController') +const HistoryController = require('./Features/History/HistoryController') +const ExportsController = require('./Features/Exports/ExportsController') +const PasswordResetRouter = require('./Features/PasswordReset/PasswordResetRouter') +const StaticPagesRouter = require('./Features/StaticPages/StaticPagesRouter') +const ChatController = require('./Features/Chat/ChatController') +const BlogController = require('./Features/Blog/BlogController') +const Modules = require('./infrastructure/Modules') +const RateLimiterMiddleware = require('./Features/Security/RateLimiterMiddleware') +const CooldownMiddleware = require('./Features/Cooldown/CooldownMiddleware') +const RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter') +const InactiveProjectController = require('./Features/InactiveData/InactiveProjectController') +const ContactRouter = require('./Features/Contacts/ContactRouter') +const ReferencesController = require('./Features/References/ReferencesController') +const AuthorizationMiddleware = require('./Features/Authorization/AuthorizationMiddleware') +const BetaProgramController = require('./Features/BetaProgram/BetaProgramController') +const SudoModeController = require('./Features/SudoMode/SudoModeController') +const SudoModeMiddleware = require('./Features/SudoMode/SudoModeMiddleware') +const AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') +const AnnouncementsController = require('./Features/Announcements/AnnouncementsController') +const MetaController = require('./Features/Metadata/MetaController') +const TokenAccessController = require('./Features/TokenAccess/TokenAccessController') +const Features = require('./infrastructure/Features') +const LinkedFilesRouter = require('./Features/LinkedFiles/LinkedFilesRouter') +const TemplatesRouter = require('./Features/Templates/TemplatesRouter') +const InstitutionsController = require('./Features/Institutions/InstitutionsController') +const UserMembershipRouter = require('./Features/UserMembership/UserMembershipRouter') + +const logger = require('logger-sharelatex') +const _ = require('underscore') + +module.exports = Router = class Router { + constructor(webRouter, privateApiRouter, publicApiRouter) { + if (!Settings.allowPublicAccess) { + webRouter.all('*', AuthenticationController.requireGlobalLogin) + } + + webRouter.get('/login', UserPagesController.loginPage) + AuthenticationController.addEndpointToLoginWhitelist('/login') + + webRouter.post('/login', AuthenticationController.passportLogin) + + webRouter.get('/logout', UserPagesController.logoutPage) + webRouter.post('/logout', UserController.logout) + + webRouter.get('/restricted', AuthorizationMiddleware.restricted) + + if (Features.hasFeature('registration')) { + webRouter.get('/register', UserPagesController.registerPage) + AuthenticationController.addEndpointToLoginWhitelist('/register') + } + + EditorRouter.apply(webRouter, privateApiRouter) + CollaboratorsRouter.apply(webRouter, privateApiRouter) + SubscriptionRouter.apply(webRouter, privateApiRouter, publicApiRouter) + UploadsRouter.apply(webRouter, privateApiRouter) + PasswordResetRouter.apply(webRouter, privateApiRouter) + StaticPagesRouter.apply(webRouter, privateApiRouter) + RealTimeProxyRouter.apply(webRouter, privateApiRouter) + ContactRouter.apply(webRouter, privateApiRouter) + AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter) + LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter) + TemplatesRouter.apply(webRouter) + UserMembershipRouter.apply(webRouter) + + Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter) + + if (Settings.enableSubscriptions) { + webRouter.get( + '/user/bonus', + AuthenticationController.requireLogin(), + ReferalController.bonus + ) + } + + if (Settings.overleaf == null) { + webRouter.get('/blog', BlogController.getIndexPage) + webRouter.get('/blog/*', BlogController.getPage) + } + + webRouter.get('/user/activate', UserPagesController.activateAccountPage) + AuthenticationController.addEndpointToLoginWhitelist('/user/activate') + + webRouter.get( + '/user/settings', + AuthenticationController.requireLogin(), + SudoModeMiddleware.protectPage, + UserPagesController.settingsPage + ) + webRouter.post( + '/user/settings', + AuthenticationController.requireLogin(), + UserController.updateUserSettings + ) + webRouter.post( + '/user/password/update', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'change-password', + maxRequests: 10, + timeInterval: 60 + }), + UserController.changePassword + ) + webRouter.get( + '/user/emails', + AuthenticationController.requireLogin(), + UserEmailsController.list + ) + webRouter.get('/user/emails/confirm', UserEmailsController.showConfirm) + webRouter.post( + '/user/emails/confirm', + RateLimiterMiddleware.rateLimit({ + endpointName: 'confirm-email', + maxRequests: 10, + timeInterval: 60 + }), + UserEmailsController.confirm + ) + webRouter.post( + '/user/emails/resend_confirmation', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'resend-confirmation', + maxRequests: 10, + timeInterval: 60 + }), + UserEmailsController.resendConfirmation + ) + + if (Features.hasFeature('affiliations')) { + webRouter.post( + '/user/emails', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-email', + maxRequests: 10, + timeInterval: 60 + }), + UserEmailsController.add + ) + webRouter.post( + '/user/emails/delete', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'delete-email', + maxRequests: 10, + timeInterval: 60 + }), + UserEmailsController.remove + ) + webRouter.post( + '/user/emails/default', + AuthenticationController.requireLogin(), + UserEmailsController.setDefault + ) + webRouter.post( + '/user/emails/endorse', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'endorse-email', + maxRequests: 30, + timeInterval: 60 + }), + UserEmailsController.endorse + ) + } + + webRouter.get( + '/user/sessions', + AuthenticationController.requireLogin(), + SudoModeMiddleware.protectPage, + UserPagesController.sessionsPage + ) + webRouter.post( + '/user/sessions/clear', + AuthenticationController.requireLogin(), + UserController.clearSessions + ) + + webRouter.delete( + '/user/newsletter/unsubscribe', + AuthenticationController.requireLogin(), + UserController.unsubscribe + ) + webRouter.post( + '/user/delete', + RateLimiterMiddleware.rateLimit({ + endpointName: 'delete-user', + maxRequests: 10, + timeInterval: 60 + }), + AuthenticationController.requireLogin(), + UserController.tryDeleteUser + ) + + webRouter.get( + '/user/personal_info', + AuthenticationController.requireLogin(), + UserInfoController.getLoggedInUsersPersonalInfo + ) + privateApiRouter.get( + '/user/:user_id/personal_info', + AuthenticationController.httpAuth, + UserInfoController.getPersonalInfo + ) + + webRouter.get( + '/user/reconfirm', + UserPagesController.renderReconfirmAccountPage + ) + // for /user/reconfirm POST, see password router + + webRouter.get( + '/user/projects', + AuthenticationController.requireLogin(), + ProjectController.userProjectsJson + ) + webRouter.get( + '/project/:Project_id/entities', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.projectEntitiesJson + ) + + webRouter.get( + '/project', + AuthenticationController.requireLogin(), + ProjectController.projectListPage + ) + webRouter.post( + '/project/new', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-project', + maxRequests: 20, + timeInterval: 60 + }), + ProjectController.newProject + ) + + webRouter.get( + '/Project/:Project_id', + RateLimiterMiddleware.rateLimit({ + endpointName: 'open-project', + params: ['Project_id'], + maxRequests: 15, + timeInterval: 60 + }), + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.loadEditor + ) + webRouter.get( + '/Project/:Project_id/file/:File_id', + AuthorizationMiddleware.ensureUserCanReadProject, + FileStoreController.getFile + ) + webRouter.post( + '/project/:Project_id/settings', + AuthorizationMiddleware.ensureUserCanWriteProjectSettings, + ProjectController.updateProjectSettings + ) + webRouter.post( + '/project/:Project_id/settings/admin', + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.updateProjectAdminSettings + ) + + webRouter.post( + '/project/:Project_id/compile', + RateLimiterMiddleware.rateLimit({ + endpointName: 'compile-project-http', + params: ['Project_id'], + maxRequests: 800, + timeInterval: 60 * 60 + }), + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.compile + ) + + webRouter.post( + '/project/:Project_id/compile/stop', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.stopCompile + ) + + // LEGACY: Used by the web download buttons, adds filename header, TODO: remove at some future date + webRouter.get( + '/project/:Project_id/output/output.pdf', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.downloadPdf + ) + + // PDF Download button + webRouter.get( + /^\/download\/project\/([^\/]*)\/output\/output\.pdf$/, + function(req, res, next) { + const params = { Project_id: req.params[0] } + req.params = params + return next() + }, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.downloadPdf + ) + + // PDF Download button for specific build + webRouter.get( + /^\/download\/project\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/output\.pdf$/, + function(req, res, next) { + const params = { + Project_id: req.params[0], + build_id: req.params[1] + } + req.params = params + return next() + }, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.downloadPdf + ) + + // Used by the pdf viewers + webRouter.get( + /^\/project\/([^\/]*)\/output\/(.*)$/, + function(req, res, next) { + const params = { + Project_id: req.params[0], + file: req.params[1] + } + req.params = params + return next() + }, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.getFileFromClsi + ) + // direct url access to output files for a specific build (query string not required) + webRouter.get( + /^\/project\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, + function(req, res, next) { + const params = { + Project_id: req.params[0], + build_id: req.params[1], + file: req.params[2] + } + req.params = params + return next() + }, + AuthorizationMiddleware.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\/(.*)$/, + function(req, res, next) { + const params = { + Project_id: req.params[0], + user_id: req.params[1], + file: req.params[2] + } + req.params = params + return next() + }, + AuthorizationMiddleware.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\/(.*)$/, + function(req, res, next) { + const params = { + Project_id: req.params[0], + user_id: req.params[1], + build_id: req.params[2], + file: req.params[3] + } + req.params = params + return next() + }, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.getFileFromClsi + ) + + webRouter.delete( + '/project/:Project_id/output', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.deleteAuxFiles + ) + webRouter.get( + '/project/:Project_id/sync/code', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.proxySyncCode + ) + webRouter.get( + '/project/:Project_id/sync/pdf', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.proxySyncPdf + ) + webRouter.get( + '/project/:Project_id/wordcount', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.wordCount + ) + + webRouter.delete( + '/Project/:Project_id', + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.deleteProject + ) + webRouter.post( + '/Project/:Project_id/restore', + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.restoreProject + ) + webRouter.post( + '/Project/:Project_id/clone', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.cloneProject + ) + + webRouter.post( + '/project/:Project_id/rename', + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.renameProject + ) + + webRouter.get( + '/project/:Project_id/updates', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApiAndInjectUserDetails + ) + webRouter.get( + '/project/:Project_id/doc/:doc_id/diff', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApi + ) + webRouter.get( + '/project/:Project_id/diff', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApiAndInjectUserDetails + ) + webRouter.get( + '/project/:Project_id/filetree/diff', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApi + ) + webRouter.post( + '/project/:Project_id/doc/:doc_id/version/:version_id/restore', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApi + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/restore', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.restoreDocFromDeletedDoc + ) + webRouter.post( + '/project/:project_id/restore_file', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.restoreFileFromV2 + ) + webRouter.get( + '/project/:project_id/version/:version/zip', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.downloadZipOfVersion + ) + privateApiRouter.post( + '/project/:Project_id/history/resync', + AuthenticationController.httpAuth, + HistoryController.resyncProjectHistory + ) + + webRouter.get( + '/project/:Project_id/labels', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.ensureProjectHistoryEnabled, + HistoryController.getLabels + ) + webRouter.post( + '/project/:Project_id/labels', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.selectHistoryApi, + HistoryController.ensureProjectHistoryEnabled, + HistoryController.createLabel + ) + webRouter.delete( + '/project/:Project_id/labels/:label_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.selectHistoryApi, + HistoryController.ensureProjectHistoryEnabled, + HistoryController.deleteLabel + ) + + webRouter.post( + '/project/:project_id/export/:brand_variation_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ExportsController.exportProject + ) + webRouter.get( + '/project/:project_id/export/:export_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ExportsController.exportStatus + ) + webRouter.get( + '/project/:project_id/export/:export_id/:type', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ExportsController.exportDownload + ) + + webRouter.get( + '/Project/:Project_id/download/zip', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectDownloadsController.downloadProject + ) + webRouter.get( + '/project/download/zip', + AuthorizationMiddleware.ensureUserCanReadMultipleProjects, + ProjectDownloadsController.downloadMultipleProjects + ) + + webRouter.get( + '/project/:project_id/metadata', + AuthorizationMiddleware.ensureUserCanReadProject, + AuthenticationController.requireLogin(), + MetaController.getMetadata + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/metadata', + AuthorizationMiddleware.ensureUserCanReadProject, + AuthenticationController.requireLogin(), + MetaController.broadcastMetadataForDoc + ) + + webRouter.get( + '/tag', + AuthenticationController.requireLogin(), + TagsController.getAllTags + ) + webRouter.post( + '/tag', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-tag', + maxRequests: 30, + timeInterval: 60 + }), + TagsController.createTag + ) + webRouter.post( + '/tag/:tag_id/rename', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'rename-tag', + maxRequests: 30, + timeInterval: 60 + }), + TagsController.renameTag + ) + webRouter.delete( + '/tag/:tag_id', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'delete-tag', + maxRequests: 30, + timeInterval: 60 + }), + TagsController.deleteTag + ) + webRouter.post( + '/tag/:tag_id/project/:project_id', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-project-to-tag', + maxRequests: 30, + timeInterval: 60 + }), + TagsController.addProjectToTag + ) + webRouter.delete( + '/tag/:tag_id/project/:project_id', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'remove-project-from-tag', + maxRequests: 30, + timeInterval: 60 + }), + TagsController.removeProjectFromTag + ) + + webRouter.get( + '/notifications', + AuthenticationController.requireLogin(), + NotificationsController.getAllUnreadNotifications + ) + webRouter.delete( + '/notifications/:notification_id', + AuthenticationController.requireLogin(), + NotificationsController.markNotificationAsRead + ) + + webRouter.get( + '/announcements', + AuthenticationController.requireLogin(), + AnnouncementsController.getUndreadAnnouncements + ) + + // Deprecated in favour of /internal/project/:project_id but still used by versioning + privateApiRouter.get( + '/project/:project_id/details', + AuthenticationController.httpAuth, + ProjectApiController.getProjectDetails + ) + + // New 'stable' /internal API end points + privateApiRouter.get( + '/internal/project/:project_id', + AuthenticationController.httpAuth, + ProjectApiController.getProjectDetails + ) + privateApiRouter.get( + '/internal/project/:Project_id/zip', + AuthenticationController.httpAuth, + ProjectDownloadsController.downloadProject + ) + privateApiRouter.get( + '/internal/project/:project_id/compile/pdf', + AuthenticationController.httpAuth, + CompileController.compileAndDownloadPdf + ) + + privateApiRouter.post( + '/internal/deactivateOldProjects', + AuthenticationController.httpAuth, + InactiveProjectController.deactivateOldProjects + ) + privateApiRouter.post( + '/internal/project/:project_id/deactivate', + AuthenticationController.httpAuth, + InactiveProjectController.deactivateProject + ) + + webRouter.get( + /^\/internal\/project\/([^\/]*)\/output\/(.*)$/, + function(req, res, next) { + const params = { + Project_id: req.params[0], + file: req.params[1] + } + req.params = params + return next() + }, + AuthenticationController.httpAuth, + CompileController.getFileFromClsi + ) + + privateApiRouter.get( + '/project/:Project_id/doc/:doc_id', + AuthenticationController.httpAuth, + DocumentController.getDocument + ) + privateApiRouter.post( + '/project/:Project_id/doc/:doc_id', + AuthenticationController.httpAuth, + DocumentController.setDocument + ) + + privateApiRouter.post( + '/user/:user_id/update/*', + AuthenticationController.httpAuth, + TpdsController.mergeUpdate + ) + privateApiRouter.delete( + '/user/:user_id/update/*', + AuthenticationController.httpAuth, + TpdsController.deleteUpdate + ) + + privateApiRouter.post( + '/project/:project_id/contents/*', + AuthenticationController.httpAuth, + TpdsController.updateProjectContents + ) + privateApiRouter.delete( + '/project/:project_id/contents/*', + AuthenticationController.httpAuth, + TpdsController.deleteProjectContents + ) + + webRouter.post( + '/spelling/check', + AuthenticationController.requireLogin(), + SpellingController.proxyRequestToSpellingApi + ) + webRouter.post( + '/spelling/learn', + AuthenticationController.requireLogin(), + SpellingController.proxyRequestToSpellingApi + ) + + webRouter.get( + '/project/:project_id/messages', + AuthorizationMiddleware.ensureUserCanReadProject, + ChatController.getMessages + ) + webRouter.post( + '/project/:project_id/messages', + AuthorizationMiddleware.ensureUserCanReadProject, + RateLimiterMiddleware.rateLimit({ + endpointName: 'send-chat-message', + maxRequests: 100, + timeInterval: 60 + }), + ChatController.sendMessage + ) + + webRouter.post( + '/project/:Project_id/references/index', + AuthorizationMiddleware.ensureUserCanReadProject, + RateLimiterMiddleware.rateLimit({ + endpointName: 'index-project-references', + maxRequests: 30, + timeInterval: 60 + }), + ReferencesController.index + ) + webRouter.post( + '/project/:Project_id/references/indexAll', + AuthorizationMiddleware.ensureUserCanReadProject, + RateLimiterMiddleware.rateLimit({ + endpointName: 'index-all-project-references', + maxRequests: 30, + timeInterval: 60 + }), + ReferencesController.indexAll + ) + + // disable beta program while v2 is in beta + 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 + ) + webRouter.get( + '/confirm-password', + AuthenticationController.requireLogin(), + SudoModeController.sudoModePrompt + ) + webRouter.post( + '/confirm-password', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'confirm-password', + maxRequests: 10, + timeInterval: 60 + }), + SudoModeController.submitPassword + ) + + // New "api" endpoints. Started as a way for v1 to call over to v2 (for + // long-term features, as opposed to the nominally temporary ones in the + // overleaf-integration module), but may expand beyond that role. + publicApiRouter.post( + '/api/clsi/compile/:submission_id', + AuthenticationController.httpAuth, + CompileController.compileSubmission + ) + publicApiRouter.get( + /^\/api\/clsi\/compile\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, + function(req, res, next) { + const params = { + submission_id: req.params[0], + build_id: req.params[1], + file: req.params[2] + } + req.params = params + return next() + }, + AuthenticationController.httpAuth, + CompileController.getFileFromClsiWithoutUser + ) + publicApiRouter.post( + '/api/institutions/confirm_university_domain', + RateLimiterMiddleware.rateLimit({ + endpointName: 'confirm-university-domain', + maxRequests: 1, + timeInterval: 60 + }), + AuthenticationController.httpAuth, + InstitutionsController.confirmDomain + ) + + webRouter.get('/chrome', function(req, res, next) { + // Match v1 behaviour - this is used for a Chrome web app + if (AuthenticationController.isUserLoggedIn(req)) { + return res.redirect('/project') + } else { + return res.redirect('/register') + } + }) + + // Admin Stuff + webRouter.get( + '/admin', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.index + ) + webRouter.get( + '/admin/user', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + (req, res) => res.redirect('/admin/register') + ) // this gets removed by admin-panel addon + webRouter.get( + '/admin/register', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.registerNewUser + ) + webRouter.post( + '/admin/register', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + UserController.register + ) + webRouter.post( + '/admin/closeEditor', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.closeEditor + ) + webRouter.post( + '/admin/dissconectAllUsers', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.dissconectAllUsers + ) + webRouter.post( + '/admin/syncUserToSubscription', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.syncUserToSubscription + ) + webRouter.post( + '/admin/flushProjectToTpds', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.flushProjectToTpds + ) + webRouter.post( + '/admin/pollDropboxForUser', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.pollDropboxForUser + ) + webRouter.post( + '/admin/messages', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.createMessage + ) + webRouter.post( + '/admin/messages/clear', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.clearMessages + ) + + privateApiRouter.post( + '/disconnectAllUsers', + AdminController.dissconectAllUsers + ) + + privateApiRouter.get('/perfTest', (req, res) => res.send('hello')) + + publicApiRouter.get('/status', (req, res) => + res.send('web sharelatex is alive (web)') + ) + privateApiRouter.get('/status', (req, res) => + res.send('web sharelatex is alive (api)') + ) + + webRouter.get('/dev/csrf', (req, res) => res.send(res.locals.csrfToken)) + + publicApiRouter.get('/health_check', HealthCheckController.check) + privateApiRouter.get('/health_check', HealthCheckController.check) + + publicApiRouter.get('/health_check/redis', HealthCheckController.checkRedis) + privateApiRouter.get( + '/health_check/redis', + HealthCheckController.checkRedis + ) + + publicApiRouter.get('/health_check/mongo', HealthCheckController.checkMongo) + privateApiRouter.get( + '/health_check/mongo', + HealthCheckController.checkMongo + ) + + webRouter.get( + '/status/compiler/:Project_id', + AuthorizationMiddleware.ensureUserCanReadProject, + function(req, res) { + const project_id = req.params.Project_id + const sendRes = _.once(function(statusCode, message) { + res.status(statusCode) + res.send(message) + return ClsiCookieManager.clearServerId(project_id) + }) // force every compile to a new server + // set a timeout + var handler = setTimeout(function() { + sendRes(500, 'Compiler timed out') + return (handler = null) + }, 10000) + // use a valid user id for testing + const test_user_id = '123456789012345678901234' + // run the compile + return CompileManager.compile(project_id, test_user_id, {}, function( + error, + status + ) { + if (handler != null) { + clearTimeout(handler) + } + if (error != null) { + return sendRes(500, `Compiler returned error ${error.message}`) + } else if (status === 'success') { + return sendRes(200, 'Compiler returned in less than 10 seconds') + } else { + return sendRes(500, `Compiler returned failure ${status}`) + } + }) + } + ) + + webRouter.get('/no-cache', function(req, res, next) { + res.header('Cache-Control', 'max-age=0') + return res.sendStatus(404) + }) + + webRouter.get('/oops-express', (req, res, next) => + next(new Error('Test error')) + ) + webRouter.get('/oops-internal', function(req, res, next) { + throw new Error('Test error') + }) + webRouter.get('/oops-mongo', (req, res, next) => + require('./models/Project').Project.findOne({}, function() { + throw new Error('Test error') + }) + ) + + privateApiRouter.get('/opps-small', function(req, res, next) { + logger.err('test error occured') + return res.send() + }) + + webRouter.post('/error/client', function(req, res, next) { + logger.warn( + { err: req.body.error, meta: req.body.meta }, + 'client side error' + ) + metrics.inc('client-side-error') + return res.sendStatus(204) + }) + + webRouter.get( + '/read/:read_only_token([a-z]+)', + RateLimiterMiddleware.rateLimit({ + endpointName: 'read-only-token', + maxRequests: 15, + timeInterval: 60 + }), + TokenAccessController.readOnlyToken + ) + + webRouter.get( + '/:read_and_write_token([0-9]+[a-z]+)', + RateLimiterMiddleware.rateLimit({ + endpointName: 'read-and-write-token', + maxRequests: 15, + timeInterval: 60 + }), + TokenAccessController.readAndWriteToken + ) + + webRouter.get('*', ErrorController.notFound) + } +} diff --git a/services/web/bin/unit_test b/services/web/bin/unit_test index 5188374896..6ad1f6b8a2 100755 --- a/services/web/bin/unit_test +++ b/services/web/bin/unit_test @@ -3,12 +3,12 @@ set -e; MOCHA="node_modules/.bin/mocha --exit --recursive --reporter spec --require test/unit/bootstrap.js" -$MOCHA "$@" test/unit/js +$MOCHA "$@" test/unit/src for dir in modules/*; do - if [ -d $dir/test/unit/js ]; then - $MOCHA "$@" $dir/test/unit/js + if [ -d $dir/test/unit/src ]; then + $MOCHA "$@" $dir/test/unit/src fi done diff --git a/services/web/decaffeinate.sh b/services/web/decaffeinate.sh index 6a06d54f27..23136ca22a 100755 --- a/services/web/decaffeinate.sh +++ b/services/web/decaffeinate.sh @@ -1,57 +1,182 @@ +#!/usr/local/bin/zsh set -ex -npx bulk-decaffeinate convert --dir public/coffee +echo "----------------------------------------" +echo "-------GIT CLEANING UNUSED FILES--------" +echo "----------------------------------------" -for module in modules/**/public/coffee; do +git clean -fd + +echo "----------------------------------------" +echo "--------------ENTRY FILE----------------" +echo "----------------------------------------" + +npx bulk-decaffeinate convert --file app.coffee + +for entryPoint in modules/**/index.coffee; do + npx bulk-decaffeinate convert --file $entryPoint +done + +npx bulk-decaffeinate clean + +npx prettier-eslint 'app.js' --write + +for entryPoint in modules/**/index.js; do + npx prettier-eslint "$entryPoint" --write +done + +git add . +git commit -m "Prettier: convert app.js & index.js decaffeinated files to Prettier format" + +echo "----------------------------------------" +echo "------------GRUNTFILE FILE--------------" +echo "----------------------------------------" + +npx bulk-decaffeinate convert --file Gruntfile.coffee + +npx bulk-decaffeinate clean + +npx prettier-eslint 'Gruntfile.js' --write + +git add . +git commit -m "Prettier: convert Gruntfile.coffee decaffeinated files to Prettier format" + +echo "----------------------------------------" +echo "------------------APP-------------------" +echo "----------------------------------------" + +npx bulk-decaffeinate convert --dir app/coffee + +for module in modules/**/app/coffee; do npx bulk-decaffeinate convert --dir $module done npx bulk-decaffeinate clean -git mv public/coffee public/src +git mv app/coffee app/src -for module in modules/**/public; do +for module in modules/**/app; do if [ -e $module/coffee ]; then git mv $module/coffee $module/src fi done -git commit -m "Rename public/coffee dir to public/src" +git commit -m "Rename app/coffee dir to app/src" -npx prettier-eslint 'public/src/**/*.js' --write +npx prettier-eslint 'app/src/**/*.js' --write -for module in modules/**/public/src; do +for module in modules/**/app/src; do npx prettier-eslint "$module/**/*.js" --write done git add . -git commit -m "Prettier: convert public/src decaffeinated files to Prettier format" +git commit -m "Prettier: convert app/src decaffeinated files to Prettier format" -npx bulk-decaffeinate convert --dir test/unit_frontend/coffee +echo "----------------------------------------" +echo "--------------UNIT TESTS----------------" +echo "----------------------------------------" -for module in modules/**/test/unit_frontend/coffee; do +npx bulk-decaffeinate convert --dir test/unit/coffee + +for module in modules/**/test/unit/coffee; do npx bulk-decaffeinate convert --dir $module done npx bulk-decaffeinate clean -git mv test/unit_frontend/coffee test/unit_frontend/src +git mv test/unit/coffee test/unit/src -for module in modules/**/test/unit_frontend; do +for module in modules/**/test/unit; do if [ -e $module/coffee ]; then git mv $module/coffee $module/src fi done -git commit -m "Rename test/unit_frontend/coffee to test/unit_frontend/src" +git commit -m "Rename test/unit/coffee to test/unit/src" -npx prettier-eslint 'test/unit_frontend/src/**/*.js' --write +npx prettier-eslint 'test/unit/src/**/*.js' --write -for module in modules/**/test/unit_frontend/src; do +for module in modules/**/test/unit/src; do npx prettier-eslint "$module/**/*.js" --write done git add . -git commit -m "Prettier: convert test/unit_frontend decaffeinated files to Prettier format" +git commit -m "Prettier: convert test/unit decaffeinated files to Prettier format" + +echo "----------------------------------------" +echo "-----------ACCEPTANCE TESTS-------------" +echo "----------------------------------------" + +npx bulk-decaffeinate convert --dir test/acceptance/coffee + +for module in modules/**/test/acceptance/coffee; do + npx bulk-decaffeinate convert --dir $module +done + +npx bulk-decaffeinate clean + +git mv test/acceptance/coffee test/acceptance/src + +for module in modules/**/test/acceptance; do + if [ -e $module/coffee ]; then + git mv $module/coffee $module/src + fi +done + +git commit -m "Rename test/acceptance/coffee to test/acceptance/src" + +npx prettier-eslint 'test/acceptance/src/**/*.js' --write + +for module in modules/**/test/acceptance/src; do + npx prettier-eslint "$module/**/*.js" --write +done + +git add . +git commit -m "Prettier: convert test/acceptance decaffeinated files to Prettier format" + +echo "----------------------------------------" +echo "-------------SMOKE TESTS----------------" +echo "----------------------------------------" + +npx bulk-decaffeinate convert --dir test/smoke/coffee + +npx bulk-decaffeinate clean + +git mv test/smoke/coffee test/smoke/src + +git commit -m "Rename test/smoke/coffee to test/smoke/src" + +npx prettier-eslint 'test/smoke/src/**/*.js' --write + +git add . +git commit -m "Prettier: convert test/smoke decaffeinated files to Prettier format" + +echo "----------------------------------------" +echo "-----------FIX REQUIRE PATHS------------" +echo "----------------------------------------" + +perl -i.bak -pe "s/([\'\"\`].*)\/app\/js(.*[\'\"\`])/\1\/app\/src\2/g" app.js +rm app.js.bak + +perl -i.bak -pe "s/([\'\"\`].*)\/app\/js(.*[\'\"\`])/\1\/app\/src\2/g" Gruntfile.js +rm Gruntfile.js.bak + +perl -i.bak -pe "s/([\'\"\`].*)\/app\/js(.*[\'\"\`])/\1\/app\/src\2/g" modules/**/index.js +rm modules/**/index.js.bak + +perl -i.bak -pe "s/([\'\"\`].*)\/app\/js(.*[\'\"\`])/\1\/app\/src\2/g" **/src/**/*.js +rm **/src/**/*.js.bak + +perl -i.bak -pe "s/([\'\"\`].*)\/test\/acceptance\/js(.*[\'\"\`])/\1\/test\/acceptance\/src\2/g" **/src/**/*.js +rm **/src/**/*.js.bak + +perl -i.bak -pe "s/([\'\"\`].*)test\/smoke\/js(.*[\'\"\`])/\1test\/smoke\/src\2/g" **/src/**/*.js +rm **/src/**/*.js.bak + +# Fix formatting after rewriting paths - extra character can make a difference +make format_fix + +git add . +git commit -m "Fix require paths in modules after decaffeination" || true echo "done" diff --git a/services/web/nodemon.frontend.json b/services/web/nodemon.frontend.json index f341cfcdb6..accd10054a 100644 --- a/services/web/nodemon.frontend.json +++ b/services/web/nodemon.frontend.json @@ -1,9 +1,4 @@ { - "ignore": [ - ".git", - "node_modules/" - ], - "verbose": true, "exec": "make compile || exit 1", "watch": [ "public/src/", diff --git a/services/web/nodemon.json b/services/web/nodemon.json index 490548f864..ebc4c4efaa 100644 --- a/services/web/nodemon.json +++ b/services/web/nodemon.json @@ -1,17 +1,8 @@ { - "ignore": [ - ".git", - "node_modules/" - ], - "verbose": true, - "execMap": { - "js": "npm run start" - }, "watch": [ - "app/coffee/", - "app.coffee", - "modules/*/app/coffee/", + "app/src/", + "app.js", + "modules/*/app/src/", "config/" - ], - "ext": "coffee" + ] } \ No newline at end of file diff --git a/services/web/package.json b/services/web/package.json index 046702b46f..2f3721b384 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -11,12 +11,11 @@ }, "scripts": { "test:acceptance:run_dir": "mocha --recursive --reporter spec --timeout 25000 --exit --grep=$MOCHA_GREP $@", - "test:unit": "npm -q run compile:app && bin/unit_test --grep=$MOCHA_GREP $@", + "test:unit": "bin/unit_test --grep=$MOCHA_GREP $@", "test:unit:ci": "bin/unit_test --timeout 10000", - "test:unit:app": "npm -q run compile:app && bin/unit_test_app $@", + "test:unit:app": "bin/unit_test_app $@", "test:frontend": "karma start", "compile": "make compile", - "compile:app": "make compile_app", "start": "npm -q run compile && node $NODE_APP_OPTIONS app.js", "nodemon": "nodemon --config nodemon.json", "nodemon:frontend": "nodemon --config nodemon.frontend.json", diff --git a/services/web/test/acceptance/coffee/ApiClsiTests.coffee b/services/web/test/acceptance/coffee/ApiClsiTests.coffee deleted file mode 100644 index d19e11f933..0000000000 --- a/services/web/test/acceptance/coffee/ApiClsiTests.coffee +++ /dev/null @@ -1,88 +0,0 @@ -expect = require("chai").expect -request = require './helpers/request' -Settings = require "settings-sharelatex" - -auth = new Buffer('sharelatex:password').toString("base64") -authed_request = request.defaults - headers: - Authorization: "Basic #{auth}" - - -describe 'ApiClsiTests', -> - describe 'compile', -> - before (done) -> - @compileSpec = - compile: - options: - compiler: 'pdflatex' - timeout: 60 - rootResourcePath: 'main.tex' - resources: [ - path: 'main/tex' - content: "\\documentclass{article}\n\\begin{document}\nHello World\n\\end{document}" - , - path: 'image.png' - url: 'www.example.com/image.png' - modified: 123456789 - ] - done() - - describe 'valid request', -> - it 'returns success and a list of output files', (done) -> - authed_request.post { - uri: '/api/clsi/compile/abcd' - json: @compileSpec - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - expect(response.body).to.deep.equal { - status: 'success' - outputFiles: [ - path: 'project.pdf' - url: '/project/abcd/build/1234/output/project.pdf' - type: 'pdf' - build: 1234 - , - path: 'project.log' - url: '/project/abcd/build/1234/output/project.log' - type: 'log' - build: 1234 - ] - } - done() - - describe 'unauthorized', -> - it 'returns 401', (done) -> - request.post { - uri: '/api/clsi/compile/abcd' - json: @compileSpec - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 401 - expect(response.body).to.equal 'Unauthorized' - done() - - describe 'get output', -> - describe 'valid file', -> - it 'returns the file', (done) -> - authed_request.get '/api/clsi/compile/abcd/build/1234/output/project.pdf', (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - expect(response.body).to.equal 'mock-pdf' - done() - - describe 'invalid file', -> - it 'returns 404', (done) -> - authed_request.get '/api/clsi/compile/abcd/build/1234/output/project.aux', (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 404 - expect(response.body).to.not.equal 'mock-pdf' - done() - - describe 'unauthorized', -> - it 'returns 401', (done) -> - request.get '/api/clsi/compile/abcd/build/1234/output/project.pdf', (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 401 - expect(response.body).to.not.equal 'mock-pdf' - done() diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee deleted file mode 100644 index 0814e74b8e..0000000000 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ /dev/null @@ -1,309 +0,0 @@ -expect = require("chai").expect -async = require("async") -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" - -MockDocstoreApi = require './helpers/MockDocstoreApi' -MockDocUpdaterApi = require './helpers/MockDocUpdaterApi' - -try_read_access = (user, project_id, test, callback) -> - async.series [ - (cb) -> - user.request.get "/project/#{project_id}", (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - (cb) -> - user.request.get "/project/#{project_id}/download/zip", (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - ], callback - -try_settings_write_access = (user, project_id, test, callback) -> - async.series [ - (cb) -> - user.request.post { - uri: "/project/#{project_id}/settings" - json: - compiler: "latex" - }, (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - ], callback - -try_admin_access = (user, project_id, test, callback) -> - async.series [ - (cb) -> - user.request.post { - uri: "/project/#{project_id}/rename" - json: - newProjectName: "new-name" - }, (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - (cb) -> - user.request.post { - uri: "/project/#{project_id}/settings/admin" - json: - publicAccessLevel: "private" - }, (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - ], callback - -try_content_access = (user, project_id, test, callback) -> - # The real-time service calls this end point to determine the user's - # permissions. - if user.id? - user_id = user.id - else - user_id = "anonymous-user" - request.post { - url: "/project/#{project_id}/join" - qs: {user_id} - auth: - user: settings.apis.web.user - pass: settings.apis.web.pass - sendImmediately: true - json: true - jar: false - }, (error, response, body) -> - return callback(error) if error? - test(response, body) - callback() - -expect_read_access = (user, project_id, callback) -> - async.series [ - (cb) -> - try_read_access(user, project_id, (response, body) -> - expect(response.statusCode).to.be.oneOf [200, 204] - , cb) - (cb) -> - try_content_access(user, project_id, (response, body) -> - expect(body.privilegeLevel).to.be.oneOf ["owner", "readAndWrite", "readOnly"] - , cb) - ], callback - -expect_content_write_access = (user, project_id, callback) -> - try_content_access(user, project_id, (response, body) -> - expect(body.privilegeLevel).to.be.oneOf ["owner", "readAndWrite"] - , callback) - -expect_settings_write_access = (user, project_id, callback) -> - try_settings_write_access(user, project_id, (response, body) -> - expect(response.statusCode).to.be.oneOf [200, 204] - , callback) - -expect_admin_access = (user, project_id, callback) -> - try_admin_access(user, project_id, (response, body) -> - expect(response.statusCode).to.be.oneOf [200, 204] - , callback) - -expect_no_read_access = (user, project_id, options, callback) -> - async.series [ - (cb) -> - try_read_access(user, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.match new RegExp(options.redirect_to) - , cb) - (cb) -> - try_content_access(user, project_id, (response, body) -> - expect(body.privilegeLevel).to.be.equal false - , cb) - ], callback - -expect_no_content_write_access = (user, project_id, callback) -> - try_content_access(user, project_id, (response, body) -> - expect(body.privilegeLevel).to.be.oneOf [false, "readOnly"] - , callback) - -expect_no_settings_write_access = (user, project_id, options, callback) -> - try_settings_write_access(user, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.match new RegExp(options.redirect_to) - , callback) - -expect_no_admin_access = (user, project_id, options, callback) -> - try_admin_access(user, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.match new RegExp(options.redirect_to) - , callback) - -describe "Authorization", -> - before (done) -> - @timeout(90000) - @owner = new User() - @other1 = new User() - @other2 = new User() - @anon = new User() - @site_admin = new User({email: "admin@example.com"}) - async.parallel [ - (cb) => @owner.login cb - (cb) => @other1.login cb - (cb) => @other2.login cb - (cb) => @anon.getCsrfToken cb - (cb) => - @site_admin.login (err) => - return cb(err) if error? - @site_admin.ensure_admin cb - ], done - - describe "private project", -> - before (done) -> - @owner.createProject "private-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - done() - - it "should allow the owner read access to it", (done) -> - expect_read_access @owner, @project_id, done - - it "should allow the owner write access to its content", (done) -> - expect_content_write_access @owner, @project_id, done - - it "should allow the owner write access to its settings", (done) -> - expect_settings_write_access @owner, @project_id, done - - it "should allow the owner admin access to it", (done) -> - expect_admin_access @owner, @project_id, done - - it "should not allow another user read access to the project", (done) -> - expect_no_read_access @other1, @project_id, redirect_to: "/restricted", done - - it "should not allow another user write access to its content", (done) -> - expect_no_content_write_access @other1, @project_id, done - - it "should not allow another user write access to its settings", (done) -> - expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done - - it "should not allow another user admin access to it", (done) -> - expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done - - it "should not allow anonymous user read access to it", (done) -> - expect_no_read_access @anon, @project_id, redirect_to: "/restricted", done - - it "should not allow anonymous user write access to its content", (done) -> - expect_no_content_write_access @anon, @project_id, done - - it "should not allow anonymous user write access to its settings", (done) -> - expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done - - it "should not allow anonymous user admin access to it", (done) -> - expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done - - it "should allow site admin users read access to it", (done) -> - expect_read_access @site_admin, @project_id, done - - it "should allow site admin users write access to its content", (done) -> - expect_content_write_access @site_admin, @project_id, done - - it "should allow site admin users write access to its settings", (done) -> - expect_settings_write_access @site_admin, @project_id, done - - it "should allow site admin users admin access to it", (done) -> - expect_admin_access @site_admin, @project_id, done - - - describe "shared project", -> - before (done) -> - @rw_user = @other1 - @ro_user = @other2 - @owner.createProject "private-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - @owner.addUserToProject @project_id, @ro_user, "readOnly", (error) => - return done(error) if error? - @owner.addUserToProject @project_id, @rw_user, "readAndWrite", (error) => - return done(error) if error? - done() - - it "should allow the read-only user read access to it", (done) -> - expect_read_access @ro_user, @project_id, done - - it "should not allow the read-only user write access to its content", (done) -> - expect_no_content_write_access @ro_user, @project_id, done - - it "should not allow the read-only user write access to its settings", (done) -> - expect_no_settings_write_access @ro_user, @project_id, redirect_to: "/restricted", done - - it "should not allow the read-only user admin access to it", (done) -> - expect_no_admin_access @ro_user, @project_id, redirect_to: "/restricted", done - - it "should allow the read-write user read access to it", (done) -> - expect_read_access @rw_user, @project_id, done - - it "should allow the read-write user write access to its content", (done) -> - expect_content_write_access @rw_user, @project_id, done - - it "should allow the read-write user write access to its settings", (done) -> - expect_settings_write_access @rw_user, @project_id, done - - it "should not allow the read-write user admin access to it", (done) -> - expect_no_admin_access @rw_user, @project_id, redirect_to: "/restricted", done - - describe "public read-write project", -> - before (done) -> - @owner.createProject "public-rw-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - @owner.makePublic @project_id, "readAndWrite", done - - it "should allow a user read access to it", (done) -> - expect_read_access @other1, @project_id, done - - it "should allow a user write access to its content", (done) -> - expect_content_write_access @other1, @project_id, done - - it "should not allow a user write access to its settings", (done) -> - expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done - - it "should not allow a user admin access to it", (done) -> - expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done - - it "should allow an anonymous user read access to it", (done) -> - expect_read_access @anon, @project_id, done - - it "should allow an anonymous user write access to its content", (done) -> - expect_content_write_access @anon, @project_id, done - - it "should not allow an anonymous user write access to its settings", (done) -> - expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done - - it "should not allow an anonymous user admin access to it", (done) -> - expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done - - describe "public read-only project", -> - before (done) -> - @owner.createProject "public-ro-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - @owner.makePublic @project_id, "readOnly", done - - it "should allow a user read access to it", (done) -> - expect_read_access @other1, @project_id, done - - it "should not allow a user write access to its content", (done) -> - expect_no_content_write_access @other1, @project_id, done - - it "should not allow a user write access to its settings", (done) -> - expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done - - it "should not allow a user admin access to it", (done) -> - expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done - - it "should allow an anonymous user read access to it", (done) -> - expect_read_access @anon, @project_id, done - - it "should not allow an anonymous user write access to its content", (done) -> - expect_no_content_write_access @anon, @project_id, done - - it "should not allow an anonymous user write access to its settings", (done) -> - expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done - - it "should not allow an anonymous user admin access to it", (done) -> - expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done diff --git a/services/web/test/acceptance/coffee/CloseSiteTests.coffee b/services/web/test/acceptance/coffee/CloseSiteTests.coffee deleted file mode 100644 index 5409c712b7..0000000000 --- a/services/web/test/acceptance/coffee/CloseSiteTests.coffee +++ /dev/null @@ -1,22 +0,0 @@ -Settings = require "settings-sharelatex" -chai = require "chai" -request = require "./helpers/request" - -describe "siteIsOpen", -> - describe "when siteIsOpen is default (true)", -> - it "should get page", (done) -> - request.get "/login", (error, response, body) -> - response.statusCode.should.equal 200 - done() - - describe "when siteIsOpen is false", -> - beforeEach -> - Settings.siteIsOpen = false - - afterEach -> - Settings.siteIsOpen = true - - it "should return maintenance page", (done) -> - request.get "/login", (error, response) -> - response.statusCode.should.equal 503 - done() diff --git a/services/web/test/acceptance/coffee/ExportsTests.coffee b/services/web/test/acceptance/coffee/ExportsTests.coffee deleted file mode 100644 index 790627776c..0000000000 --- a/services/web/test/acceptance/coffee/ExportsTests.coffee +++ /dev/null @@ -1,68 +0,0 @@ -expect = require('chai').expect -request = require './helpers/request' -_ = require 'underscore' - - -User = require './helpers/User' -ProjectGetter = require '../../../app/js/Features/Project/ProjectGetter.js' -ExportsHandler = require '../../../app/js/Features/Exports/ExportsHandler.js' - -MockProjectHistoryApi = require './helpers/MockProjectHistoryApi' -MockV1Api = require './helpers/MockV1Api' - -describe 'Exports', -> - before (done) -> - @brand_variation_id = '18' - @owner = new User() - @owner.login (error) => - throw error if error? - @owner.createProject 'example-project', {template: 'example'}, (error, @project_id) => - throw error if error? - done() - - describe 'exporting a project', -> - beforeEach (done) -> - @version = Math.floor(Math.random() * 10000) - MockProjectHistoryApi.setProjectVersion(@project_id, @version) - @export_id = Math.floor(Math.random() * 10000) - MockV1Api.setExportId(@export_id) - MockV1Api.clearExportParams() - @owner.request { - method: 'POST', - url: "/project/#{@project_id}/export/#{@brand_variation_id}", - json: true, - body: - title: 'title' - description: 'description' - author: 'author' - license: 'other' - showSource: true - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - @exportResponseBody = body - done() - - it 'should have sent correct data to v1', (done) -> - {project, user, destination, options} = MockV1Api.getLastExportParams() - # project details should match - expect(project.id).to.equal @project_id - expect(project.rootDocPath).to.equal '/main.tex' - # gallery details should match - expect(project.metadata.title).to.equal 'title' - expect(project.metadata.description).to.equal 'description' - expect(project.metadata.author).to.equal 'author' - expect(project.metadata.license).to.equal 'other' - expect(project.metadata.showSource).to.equal true - # version should match what was retrieved from project-history - expect(project.historyVersion).to.equal @version - # user details should match - expect(user.id).to.equal @owner.id - expect(user.email).to.equal @owner.email - # brand-variation should match - expect(destination.brandVariationId).to.equal @brand_variation_id - done() - - it 'should have returned the export ID provided by v1', (done) -> - expect(@exportResponseBody.export_v1_id).to.equal @export_id - done() diff --git a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee deleted file mode 100644 index ceef1e0c36..0000000000 --- a/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee +++ /dev/null @@ -1,213 +0,0 @@ -expect = require("chai").expect -async = require("async") -UserClient = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" -{ObjectId} = require("../../../app/js/infrastructure/mongojs") -Subscription = require("../../../app/js/models/Subscription").Subscription -User = require("../../../app/js/models/User").User -FeaturesUpdater = require("../../../app/js/Features/Subscription/FeaturesUpdater") - -MockV1Api = require "./helpers/MockV1Api" -logger = require "logger-sharelatex" -logger.logger.level("error") - -syncUserAndGetFeatures = (user, callback = (error, features) ->) -> - FeaturesUpdater.refreshFeatures user._id, false, (error) -> - return callback(error) if error? - User.findById user._id, (error, user) -> - return callback(error) if error? - features = user.toObject().features - delete features.$init # mongoose internals - return callback null, features - -describe "FeatureUpdater.refreshFeatures", -> - beforeEach (done) -> - @user = new UserClient() - @user.ensureUserExists (error) -> - throw error if error? - done() - - describe "when user has no subscriptions", -> - it "should set their features to the basic set", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - expect(features).to.deep.equal(settings.defaultFeatures) - done() - - describe "when the user has an individual subscription", -> - beforeEach -> - Subscription.create { - admin_id: @user._id - manager_ids: [@user._id] - planCode: 'collaborator' - customAccount: true - } # returns a promise - - it "should set their features to the upgraded set", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - plan = settings.plans.find (plan) -> plan.planCode == 'collaborator' - expect(features).to.deep.equal(plan.features) - done() - - describe "when the user is in a group subscription", -> - beforeEach -> - Subscription.create { - admin_id: ObjectId() - member_ids: [@user._id] - groupAccount: true - planCode: 'collaborator' - customAccount: true - } # returns a promise - - it "should set their features to the upgraded set", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - plan = settings.plans.find (plan) -> plan.planCode == 'collaborator' - expect(features).to.deep.equal(plan.features) - done() - - describe "when the user has bonus features", -> - beforeEach -> - User.update { - _id: @user._id - }, { - refered_user_count: 10 - } # returns a promise - - it "should set their features to the bonus set", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - expect(features).to.deep.equal(Object.assign( - {}, settings.defaultFeatures, settings.bonus_features[9] - )) - done() - - describe "when the user has affiliations", -> - beforeEach -> - @institutionPlan = settings.plans.find (plan) -> - plan.planCode == settings.institutionPlanCode - @email = @user.emails[0].email - @affiliationData = - email: @email - institution: { licence: 'pro_plus', confirmed: true } - - it "should not set their features if email is not confirmed", (done) -> - MockV1Api.setAffiliations [@affiliationData] - syncUserAndGetFeatures @user, (error, features) => - expect(features).to.deep.equal(settings.defaultFeatures) - done() - - it "should set their features if email is confirmed", (done) -> - MockV1Api.setAffiliations [@affiliationData] - @user.confirmEmail @email, (error) => - syncUserAndGetFeatures @user, (error, features) => - expect(features).to.deep.equal(@institutionPlan.features) - done() - - it "should not set their features if institution is not confirmed", (done) -> - @affiliationData.institution.confirmed = false - MockV1Api.setAffiliations [@affiliationData] - @user.confirmEmail @email, (error) => - syncUserAndGetFeatures @user, (error, features) => - expect(features).to.deep.equal(settings.defaultFeatures) - done() - - describe "when the user is due bonus features and has extra features that no longer apply", -> - beforeEach -> - User.update { - _id: @user._id - }, { - refered_user_count: 10, - 'features.github': true - } # returns a promise - - it "should set their features to the bonus set and downgrade the extras", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - expect(features).to.deep.equal(Object.assign( - {}, settings.defaultFeatures, settings.bonus_features[9] - )) - done() - - describe "when the user has a v1 plan", -> - beforeEach -> - MockV1Api.setUser 42, plan_name: 'free' - User.update { - _id: @user._id - }, { - overleaf: - id: 42 - } # returns a promise - - it "should set their features to the v1 plan", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - plan = settings.plans.find (plan) -> plan.planCode == 'v1_free' - expect(features).to.deep.equal(plan.features) - done() - - describe "when the user has a v1 plan and bonus features", -> - beforeEach -> - MockV1Api.setUser 42, plan_name: 'free' - User.update { - _id: @user._id - }, { - overleaf: - id: 42 - refered_user_count: 10 - } # returns a promise - - it "should set their features to the best of the v1 plan and bonus features", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - v1plan = settings.plans.find (plan) -> plan.planCode == 'v1_free' - expectedFeatures = Object.assign( - {}, v1plan.features, settings.bonus_features[9] - ) - expect(features).to.deep.equal(expectedFeatures) - done() - - describe "when the user has a group and personal subscription", -> - beforeEach (done) -> - Subscription.create { - admin_id: @user._id - manager_ids: [@user._id] - planCode: 'professional' - customAccount: true - }, (error) => - throw error if error? - Subscription.create { - admin_id: ObjectId() - member_ids: [@user._id] - groupAccount: true - planCode: 'collaborator' - customAccount: true - }, done - return - - it "should set their features to the best set", (done) -> - syncUserAndGetFeatures @user, (error, features) => - throw error if error? - plan = settings.plans.find (plan) -> plan.planCode == 'professional' - expect(features).to.deep.equal(plan.features) - done() - - describe "when the notifyV1Flag is passed", -> - beforeEach -> - User.update { - _id: @user._id - }, { - overleaf: - id: 42 - } # returns a promise - - it "should ping the v1 API end point to sync", (done) -> - FeaturesUpdater.refreshFeatures @user._id, true, (error) => - setTimeout () => - expect( - MockV1Api.syncUserFeatures.calledWith('42') - ).to.equal true - done() - , 500 diff --git a/services/web/test/acceptance/coffee/HistoryTests.coffee b/services/web/test/acceptance/coffee/HistoryTests.coffee deleted file mode 100644 index 80954f2675..0000000000 --- a/services/web/test/acceptance/coffee/HistoryTests.coffee +++ /dev/null @@ -1,47 +0,0 @@ -{expect} = require 'chai' - -{db, ObjectId} = require("../../../app/js/infrastructure/mongojs") -MockV1HistoryApi = require './helpers/MockV1HistoryApi' -User = require './helpers/User' - -describe 'History', -> - beforeEach (done) -> - @owner = new User() - @owner.login done - - describe 'zip download of version', -> - it 'should stream the zip file of a version', (done) -> - @owner.createProject 'example-project', (error, @project_id) => - return done(error) if error? - @v1_history_id = 42 - db.projects.update { - _id: ObjectId(@project_id) - }, { - $set: { - 'overleaf.history.id': @v1_history_id - } - }, (error) => - return done(error) if error? - @owner.request "/project/#{@project_id}/version/42/zip", (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - expect(response.headers['content-type']).to.equal 'application/zip' - expect(response.headers['content-disposition']).to.equal 'attachment; filename="example-project%20(Version%2042).zip"' - expect(body).to.equal "Mock zip for #{@v1_history_id} at version 42" - done() - - it 'should return 402 for non-v2-history project', (done) -> - @owner.createProject 'non-v2-project', (error, @project_id) => - return done(error) if error? - db.projects.update { - _id: ObjectId(@project_id) - }, { - $unset: { - 'overleaf.history.id': true - } - }, (error) => - return done(error) if error? - @owner.request "/project/#{@project_id}/version/42/zip", (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 402 - done() \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/Init.coffee b/services/web/test/acceptance/coffee/Init.coffee deleted file mode 100644 index 4a3ce6ad2b..0000000000 --- a/services/web/test/acceptance/coffee/Init.coffee +++ /dev/null @@ -1,5 +0,0 @@ -App = require '../../../app.js' -require("logger-sharelatex").logger.level("error") - -before (done) -> - App.listen 3000, 'localhost', done diff --git a/services/web/test/acceptance/coffee/LabelsTests.coffee b/services/web/test/acceptance/coffee/LabelsTests.coffee deleted file mode 100644 index 2c6176f574..0000000000 --- a/services/web/test/acceptance/coffee/LabelsTests.coffee +++ /dev/null @@ -1,68 +0,0 @@ -_ = require 'underscore' -{expect} = require 'chai' -{ObjectId} = require 'mongojs' -request = require './helpers/request' - -MockProjectHistoryApi = require './helpers/MockProjectHistoryApi' -User = require './helpers/User' - -describe 'Labels', -> - beforeEach (done) -> - @owner = new User() - @owner.login (error) => - throw error if error? - @owner.createProject 'example-project', {template: 'example'}, (error, @project_id) => - throw error if error? - done() - - afterEach -> - MockProjectHistoryApi.reset() - - it 'getting labels', (done) -> - label_id = new ObjectId().toString() - comment = 'a label comment' - version = 3 - MockProjectHistoryApi.addLabel @project_id, {id: label_id, comment, version} - - @owner.request { - method: 'GET' - url: "/project/#{@project_id}/labels" - json: true - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - expect(body).to.deep.equal [{ id: label_id, comment, version }] - done() - - it 'creating a label', (done) -> - comment = 'a label comment' - version = 3 - - @owner.request { - method: 'POST' - url: "/project/#{@project_id}/labels" - json: {comment, version} - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - {label_id} = body - expect( - MockProjectHistoryApi.getLabels(@project_id) - ).to.deep.equal [{id: label_id, comment, version} ] - done() - - it 'deleting a label', (done) -> - label_id = new ObjectId().toString() - comment = 'a label comment' - version = 3 - MockProjectHistoryApi.addLabel @project_id, {id: label_id, comment, version} - - @owner.request { - method: 'DELETE' - url: "/project/#{@project_id}/labels/#{label_id}" - json: true - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 204 - expect(MockProjectHistoryApi.getLabels(@project_id)).to.deep.equal [] - done() diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee deleted file mode 100644 index 676aef7439..0000000000 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ /dev/null @@ -1,450 +0,0 @@ -async = require "async" -expect = require("chai").expect -_ = require 'underscore' -mkdirp = require "mkdirp" - -Settings = require "settings-sharelatex" -MockFileStoreApi = require './helpers/MockFileStoreApi' -request = require "./helpers/request" -User = require "./helpers/User" - -MockClsiApi = require "./helpers/MockClsiApi" - - -express = require("express") -LinkedUrlProxy = express() -LinkedUrlProxy.get "/", (req, res, next) => - if req.query.url == 'http://example.com/foo' - res.send('foo foo foo') - else if req.query.url == 'http://example.com/bar' - res.send('bar bar bar') - else - res.sendStatus(404) - -describe "LinkedFiles", -> - before (done) -> - LinkedUrlProxy.listen 6543, (error) => - return done(error) if error? - @owner = new User() - @owner.login -> - mkdirp Settings.path.dumpFolder, done - - describe "creating a project linked file", -> - before (done) -> - @source_doc_name = 'test.txt' - async.series [ - (cb) => - @owner.createProject 'plf-test-one', {template: 'blank'}, (error, project_id) => - @project_one_id = project_id - cb(error) - (cb) => - @owner.getProject @project_one_id, (error, project) => - @project_one = project - @project_one_root_folder_id = project.rootFolder[0]._id.toString() - cb(error) - (cb) => - @owner.createProject 'plf-test-two', {template: 'blank'}, (error, project_id) => - @project_two_id = project_id - cb(error) - (cb) => - @owner.getProject @project_two_id, (error, project) => - @project_two = project - @project_two_root_folder_id = project.rootFolder[0]._id.toString() - cb(error) - (cb) => - @owner.createDocInProject @project_two_id, - @project_two_root_folder_id, - @source_doc_name, - (error, doc_id) => - @source_doc_id = doc_id - cb(error) - (cb) => - @owner.createDocInProject @project_two_id, - @project_two_root_folder_id, - 'some-harmless-doc.txt', - (error, doc_id) => - cb(error) - ], done - - it 'should produce a list of the users projects', (done) -> - @owner.request.get { - url: "/user/projects", - json: true - }, (err, response, body) => - expect(err).to.not.exist - expect(body).to.deep.equal { - projects: [ - { _id: @project_one_id, name: 'plf-test-one', accessLevel: 'owner' }, - { _id: @project_two_id, name: 'plf-test-two', accessLevel: 'owner' } - ] - } - done() - - it 'should produce a list of entities in the project', (done) -> - @owner.request.get { - url: "/project/#{@project_two_id}/entities", - json: true - }, (err, response, body) => - expect(err).to.not.exist - expect(body).to.deep.equal { - project_id: @project_two_id, - entities: [ - { path: '/main.tex', type: 'doc' }, - { path: '/some-harmless-doc.txt', type: 'doc' }, - { path: '/test.txt', type: 'doc' } - ] - } - done() - - it 'should import a file from the source project', (done) -> - @owner.request.post { - url: "/project/#{@project_one_id}/linked_file", - json: - name: 'test-link.txt', - parent_folder_id: @project_one_root_folder_id, - provider: 'project_file', - data: - source_project_id: @project_two_id, - source_entity_path: "/#{@source_doc_name}", - }, (error, response, body) => - expect(response.statusCode).to.equal 200 - new_file_id = body.new_file_id - @existing_file_id = new_file_id - expect(new_file_id).to.exist - @owner.getProject @project_one_id, (error, project) => - return done(error) if error? - firstFile = project.rootFolder[0].fileRefs[0] - expect(firstFile._id.toString()).to.equal(new_file_id.toString()) - expect(firstFile.linkedFileData).to.deep.equal { - provider: 'project_file', - source_project_id: @project_two_id, - source_entity_path: "/#{@source_doc_name}", - } - expect(firstFile.name).to.equal('test-link.txt') - done() - - it 'should refresh the file', (done) -> - @owner.request.post { - url: "/project/#{@project_one_id}/linked_file/#{@existing_file_id}/refresh", - json: true - }, (error, response, body) => - expect(response.statusCode).to.equal 200 - new_file_id = body.new_file_id - expect(new_file_id).to.exist - expect(new_file_id).to.not.equal @existing_file_id - @refreshed_file_id = new_file_id - @owner.getProject @project_one_id, (error, project) => - return done(error) if error? - firstFile = project.rootFolder[0].fileRefs[0] - expect(firstFile._id.toString()).to.equal(new_file_id.toString()) - expect(firstFile.name).to.equal('test-link.txt') - done() - - it 'should not allow to create a linked-file with v1 id', (done) -> - @owner.request.post { - url: "/project/#{@project_one_id}/linked_file", - json: - name: 'test-link-should-not-work.txt', - parent_folder_id: @project_one_root_folder_id, - provider: 'project_file', - data: - v1_source_doc_id: 1234 - source_entity_path: "/#{@source_doc_name}", - }, (error, response, body) => - expect(response.statusCode).to.equal 403 - expect(body).to.equal 'You do not have access to this project' - done() - - describe "with a linked project_file from a v1 project that has not been imported", -> - before (done) -> - async.series [ - (cb) => - @owner.createProject 'plf-v1-test-one', {template: 'blank'}, (error, project_id) => - @project_one_id = project_id - cb(error) - (cb) => - @owner.getProject @project_one_id, (error, project) => - @project_one = project - @project_one_root_folder_id = project.rootFolder[0]._id.toString() - @project_one.rootFolder[0].fileRefs.push { - linkedFileData: { - provider: "project_file", - v1_source_doc_id: 9999999, # We won't find this id in the database - source_entity_path: "example.jpeg" - }, - _id: "abcd", - rev: 0, - created: new Date(), - name: "example.jpeg" - } - @owner.saveProject @project_one, cb - ], done - - it 'should refuse to refresh', (done) -> - @owner.request.post { - url: "/project/#{@project_one_id}/linked_file/abcd/refresh", - json: true - }, (error, response, body) => - expect(response.statusCode).to.equal 409 - expect(body).to.equal "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file" - done() - - describe "creating a URL based linked file", -> - before (done) -> - @owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) => - throw error if error? - @project_id = project_id - @owner.getProject project_id, (error, project) => - throw error if error? - @project = project - @root_folder_id = project.rootFolder[0]._id.toString() - done() - - it "should download the URL and create a file with the contents and linkedFileData", (done) -> - @owner.request.post { - url: "/project/#{@project_id}/linked_file", - json: - provider: 'url' - data: { - url: 'http://example.com/foo' - } - parent_folder_id: @root_folder_id - name: 'url-test-file-1' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - @owner.getProject @project_id, (error, project) => - throw error if error? - file = project.rootFolder[0].fileRefs[0] - expect(file.linkedFileData).to.deep.equal({ - provider: 'url' - url: 'http://example.com/foo' - }) - @owner.request.get "/project/#{@project_id}/file/#{file._id}", (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - expect(body).to.equal "foo foo foo" - done() - - it "should replace and update a URL based linked file", (done) -> - @owner.request.post { - url: "/project/#{@project_id}/linked_file", - json: - provider: 'url' - data: { - url: 'http://example.com/foo' - } - parent_folder_id: @root_folder_id - name: 'url-test-file-2' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - @owner.request.post { - url: "/project/#{@project_id}/linked_file", - json: - provider: 'url' - data: { - url: 'http://example.com/bar' - } - parent_folder_id: @root_folder_id - name: 'url-test-file-2' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - @owner.getProject @project_id, (error, project) => - throw error if error? - file = project.rootFolder[0].fileRefs[1] - expect(file.linkedFileData).to.deep.equal({ - provider: 'url' - url: 'http://example.com/bar' - }) - @owner.request.get "/project/#{@project_id}/file/#{file._id}", (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - expect(body).to.equal "bar bar bar" - done() - - it "should return an error if the URL does not succeed", (done) -> - @owner.request.post { - url: "/project/#{@project_id}/linked_file", - json: - provider: 'url' - data: { - url: 'http://example.com/does-not-exist' - } - parent_folder_id: @root_folder_id - name: 'url-test-file-3' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 422 # unprocessable - expect(body).to.equal( - "Your URL could not be reached (404 status code). Please check it and try again." - ) - done() - - it "should return an error if the URL is invalid", (done) -> - @owner.request.post { - url: "/project/#{@project_id}/linked_file", - json: - provider: 'url' - data: { - url: "!^$%" - } - parent_folder_id: @root_folder_id - name: 'url-test-file-4' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 422 # unprocessable - expect(body).to.equal( - "Your URL is not valid. Please check it and try again." - ) - done() - - it "should return an error if the URL uses a non-http protocol", (done) -> - @owner.request.post { - url: "/project/#{@project_id}/linked_file", - json: - provider: 'url' - data: { - url: "ftp://localhost" - } - parent_folder_id: @root_folder_id - name: 'url-test-file-5' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 422 # unprocessable - expect(body).to.equal( - "Your URL is not valid. Please check it and try again." - ) - done() - - it "should accept a URL withuot a leading http://, and add it", (done) -> - @owner.request.post { - url: "/project/#{@project_id}/linked_file", - json: - provider: 'url' - data: { - url: 'example.com/foo' - } - parent_folder_id: @root_folder_id - name: 'url-test-file-6' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - @owner.getProject @project_id, (error, project) => - throw error if error? - file = _.find project.rootFolder[0].fileRefs, (file) -> - file.name == 'url-test-file-6' - expect(file.linkedFileData).to.deep.equal({ - provider: 'url' - url: 'http://example.com/foo' - }) - @owner.request.get "/project/#{@project_id}/file/#{file._id}", (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - expect(body).to.equal "foo foo foo" - done() - - # TODO: Add test for asking for host that return ENOTFOUND - # (This will probably end up handled by the proxy) - - describe "creating a linked output file", -> - before (done) -> - async.series [ - (cb) => - @owner.createProject 'output-test-one', {template: 'blank'}, (error, project_id) => - @project_one_id = project_id - cb(error) - (cb) => - @owner.getProject @project_one_id, (error, project) => - @project_one = project - @project_one_root_folder_id = project.rootFolder[0]._id.toString() - cb(error) - (cb) => - @owner.createProject 'output-test-two', {template: 'blank'}, (error, project_id) => - @project_two_id = project_id - cb(error) - (cb) => - @owner.getProject @project_two_id, (error, project) => - @project_two = project - @project_two_root_folder_id = project.rootFolder[0]._id.toString() - cb(error) - ], done - - it 'should import the project.pdf file from the source project', (done) -> - @owner.request.post { - url: "/project/#{@project_one_id}/linked_file", - json: - name: 'test.pdf', - parent_folder_id: @project_one_root_folder_id, - provider: 'project_output_file', - data: - source_project_id: @project_two_id, - source_output_file_path: "project.pdf", - build_id: '1234-abcd' - }, (error, response, body) => - new_file_id = body.new_file_id - @existing_file_id = new_file_id - expect(new_file_id).to.exist - @owner.getProject @project_one_id, (error, project) => - return done(error) if error? - firstFile = project.rootFolder[0].fileRefs[0] - expect(firstFile._id.toString()).to.equal(new_file_id.toString()) - expect(firstFile.linkedFileData).to.deep.equal { - provider: 'project_output_file', - source_project_id: @project_two_id, - source_output_file_path: "project.pdf", - build_id: '1234-abcd' - } - expect(firstFile.name).to.equal('test.pdf') - done() - - it 'should refresh the file', (done) -> - @owner.request.post { - url: "/project/#{@project_one_id}/linked_file/#{@existing_file_id}/refresh", - json: true - }, (error, response, body) => - new_file_id = body.new_file_id - expect(new_file_id).to.exist - expect(new_file_id).to.not.equal @existing_file_id - @refreshed_file_id = new_file_id - @owner.getProject @project_one_id, (error, project) => - return done(error) if error? - firstFile = project.rootFolder[0].fileRefs[0] - expect(firstFile._id.toString()).to.equal(new_file_id.toString()) - expect(firstFile.name).to.equal('test.pdf') - done() - - describe "with a linked project_output_file from a v1 project that has not been imported", -> - before (done) -> - async.series [ - (cb) => - @owner.createProject 'output-v1-test-one', {template: 'blank'}, (error, project_id) => - @project_one_id = project_id - cb(error) - (cb) => - @owner.getProject @project_one_id, (error, project) => - @project_one = project - @project_one_root_folder_id = project.rootFolder[0]._id.toString() - @project_one.rootFolder[0].fileRefs.push { - linkedFileData: { - provider: "project_output_file", - v1_source_doc_id: 9999999, # We won't find this id in the database - source_output_file_path: "project.pdf" - }, - _id: "abcdef", - rev: 0, - created: new Date(), - name: "whatever.pdf" - } - @owner.saveProject @project_one, cb - ], done - - it 'should refuse to refresh', (done) -> - @owner.request.post { - url: "/project/#{@project_one_id}/linked_file/abcdef/refresh", - json: true - }, (error, response, body) => - expect(response.statusCode).to.equal 409 - expect(body).to.equal "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file" - done() diff --git a/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee b/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee deleted file mode 100644 index 19e5f445f7..0000000000 --- a/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee +++ /dev/null @@ -1,21 +0,0 @@ -expect = require("chai").expect -async = require("async") -User = require "./helpers/User" - -describe "Project CRUD", -> - before (done) -> - @user = new User() - @user.login done - - describe "when project doesn't exist", -> - it "should return 404", (done) -> - @user.request.get "/project/aaaaaaaaaaaaaaaaaaaaaaaa", (err, res, body) -> - expect(res.statusCode).to.equal 404 - done() - - describe "when project has malformed id", -> - it "should return 404", (done) -> - @user.request.get "/project/blah", (err, res, body) -> - expect(res.statusCode).to.equal 404 - done() - \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee b/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee deleted file mode 100644 index 3e9da6559c..0000000000 --- a/services/web/test/acceptance/coffee/ProjectDuplicateNameTests.coffee +++ /dev/null @@ -1,408 +0,0 @@ -async = require "async" -expect = require("chai").expect -sinon = require "sinon" -mkdirp = require "mkdirp" -ObjectId = require("mongojs").ObjectId -Path = require "path" -fs = require "fs" -Settings = require "settings-sharelatex" -_ = require "underscore" - -ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js" - -MockDocStoreApi = require './helpers/MockDocstoreApi' -MockFileStoreApi = require './helpers/MockFileStoreApi' -request = require "./helpers/request" -User = require "./helpers/User" - -describe "ProjectDuplicateNames", -> - before (done) -> - @owner = new User() - @owner.login done - @project = {} - @callback = sinon.stub() - - describe "creating a project from the example template", -> - before (done) -> - @owner.createProject "example-project", {template: "example"}, (error, project_id) => - throw error if error? - @example_project_id = project_id - @owner.getProject project_id, (error, project) => - @project = project - @mainTexDoc = _.find(project.rootFolder[0].docs, (doc) -> doc.name is 'main.tex') - @refBibDoc = _.find(project.rootFolder[0].docs, (doc) -> doc.name is 'references.bib') - @imageFile = _.find(project.rootFolder[0].fileRefs, (file) -> file.name is 'universe.jpg') - @rootFolderId = project.rootFolder[0]._id.toString() - # create a folder called 'testfolder' - @owner.request.post { - uri: "/project/#{@example_project_id}/folder" - json: - name: "testfolder" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @testFolderId = body._id - done() - - it "should create a project", -> - expect(@project.rootFolder[0].docs.length).to.equal(2) - expect(@project.rootFolder[0].fileRefs.length).to.equal(1) - - it "should create two docs in the docstore", -> - docs = MockDocStoreApi.docs[@example_project_id] - expect(Object.keys(docs).length).to.equal(2) - - it "should create one file in the filestore", -> - files = MockFileStoreApi.files[@example_project_id] - expect(Object.keys(files).length).to.equal(1) - - describe "for an existing doc", -> - describe "trying to add a doc with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc" - json: - name: "main.tex" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to add a folder with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder" - json: - name: "main.tex" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to add a folder with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder" - json: - name: "main.tex" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "for an existing file", -> - describe "trying to add a doc with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc" - json: - name: "universe.jpg" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to add a folder with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder" - json: - name: "universe.jpg" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to upload a file with the same name", -> - before (done) -> - @owner.request.post - uri: "/project/#{@example_project_id}/upload" - json: true - qs: - folder_id: @rootFolderId - qqfilename: "universe.jpg" - formData: - qqfile: - value: fs.createReadStream Path.resolve(__dirname + '/../files/1pixel.png') - options: - filename: 'universe.jpg', - contentType: 'image/jpeg' - , (err, res, body) => - @body = body - # update the image id because we have replaced the file - @imageFile._id = @body.entity_id - done() - - it "should succeed (overwriting the file)", -> - expect(@body.success).to.equal true - # at this point the @imageFile._id has changed - - describe "for an existing folder", -> - describe "trying to add a doc with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc" - json: - name: "testfolder" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to add a folder with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder" - json: - name: "testfolder" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to upload a file with the same name", -> - before (done) -> - @owner.request.post - uri: "/project/#{@example_project_id}/upload" - json: true - qs: - folder_id: @rootFolderId - qqfilename: "universe.jpg" - formData: - qqfile: - value: fs.createReadStream Path.resolve(__dirname + '/../files/1pixel.png') - options: - filename: 'testfolder', - contentType: 'image/jpeg' - , (err, res, body) => - @body = body - done() - - it "should respond with failure status", -> - expect(@body.success).to.equal false - - - describe "for an existing doc", -> - describe "trying to rename a doc to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc/#{@refBibDoc._id}/rename" - json: - name: "main.tex" - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to rename a folder to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/rename" - json: - name: "main.tex" - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to rename a file to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/rename" - json: - name: "main.tex" - }, (err, res, body) => - @res = res - done() - - it "should respond with failure status", -> - expect(@res.statusCode).to.equal 400 - - - describe "for an existing file", -> - describe "trying to rename a doc to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc/#{@refBibDoc._id}/rename" - json: - name: "universe.jpg" - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to rename a folder to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/rename" - json: - name: "universe.jpg" - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to rename a file to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/rename" - json: - name: "universe.jpg" - }, (err, res, body) => - @res = res - done() - - it "should respond with failure status", -> - expect(@res.statusCode).to.equal 400 - - - describe "for an existing folder", -> - describe "trying to rename a doc to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc/#{@refBibDoc._id}/rename" - json: - name: "testfolder" - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to rename a folder to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/rename" - json: - name: "testfolder" - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to rename a file to the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/rename" - json: - name: "testfolder" - }, (err, res, body) => - @res = res - done() - - it "should respond with failure status", -> - expect(@res.statusCode).to.equal 400 - - - describe "for an existing folder with a file with the same name", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc" - json: - name: "main.tex" - parent_folder_id: @testFolderId - }, (err, res, body) => - @owner.request.post { - uri: "/project/#{@example_project_id}/doc" - json: - name: "universe.jpg" - parent_folder_id: @testFolderId - }, (err, res, body) => - @owner.request.post { - uri: "/project/#{@example_project_id}/folder" - json: - name: "otherFolder" - parent_folder_id: @testFolderId - }, (err, res, body) => - @subFolderId = body._id - @owner.request.post { - uri: "/project/#{@example_project_id}/folder" - json: - name: "otherFolder" - parent_folder_id: @rootFolderId - }, (err, res, body) => - @otherFolderId = body._id - done() - - describe "trying to move a doc into the folder", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/doc/#{@mainTexDoc._id}/move" - json: - folder_id: @testFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to move a file into the folder", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/file/#{@imageFile._id}/move" - json: - folder_id: @testFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to move a folder into the folder", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder/#{@otherFolderId}/move" - json: - folder_id: @testFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 - - describe "trying to move a folder into a subfolder of itself", -> - before (done) -> - @owner.request.post { - uri: "/project/#{@example_project_id}/folder/#{@testFolderId}/move" - json: - folder_id: @subFolderId - }, (err, res, body) => - @res = res - done() - - it "should respond with 400 error status", -> - expect(@res.statusCode).to.equal 400 diff --git a/services/web/test/acceptance/coffee/ProjectFeaturesTests.coffee b/services/web/test/acceptance/coffee/ProjectFeaturesTests.coffee deleted file mode 100644 index 7caa48c0b7..0000000000 --- a/services/web/test/acceptance/coffee/ProjectFeaturesTests.coffee +++ /dev/null @@ -1,61 +0,0 @@ -expect = require("chai").expect -async = require("async") -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" - -joinProject = (user_id, project_id, callback) -> - request.post { - url: "/project/#{project_id}/join" - qs: {user_id} - auth: - user: settings.apis.web.user - pass: settings.apis.web.pass - sendImmediately: true - json: true - jar: false - }, callback - -describe "ProjectFeatures", -> - - before (done) -> - @timeout(90000) - @owner = new User() - async.series [ - (cb) => @owner.login cb - ], done - - describe "with private project", -> - before (done) -> - @owner.createProject "private-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - done() - - describe "with an upgraded account", -> - before (done) -> - @owner.upgradeFeatures done - after (done) -> - @owner.defaultFeatures done - - it "should have premium features", (done) -> - joinProject @owner._id, @project_id, (error, response, body) -> - expect(body.project.features.compileGroup).to.equal "priority" - expect(body.project.features.versioning).to.equal true - expect(body.project.features.templates).to.equal true - expect(body.project.features.dropbox).to.equal true - done() - - describe "with an basic account", -> - before (done) -> - @owner.downgradeFeatures done - after (done) -> - @owner.defaultFeatures done - - it "should have basic features", (done) -> - joinProject @owner._id, @project_id, (error, response, body) -> - expect(body.project.features.compileGroup).to.equal "standard" - expect(body.project.features.versioning).to.equal false - expect(body.project.features.templates).to.equal false - expect(body.project.features.dropbox).to.equal false - done() diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee deleted file mode 100644 index 4d40540064..0000000000 --- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee +++ /dev/null @@ -1,501 +0,0 @@ -expect = require("chai").expect -Async = require("async") -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" -CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler" - - -createInvite = (sendingUser, projectId, email, callback=(err, invite)->) -> - sendingUser.getCsrfToken (err) -> - return callback(err) if err - sendingUser.request.post { - uri: "/project/#{projectId}/invite", - json: - email: email - privileges: 'readAndWrite' - }, (err, response, body) -> - return callback(err) if err - expect(response.statusCode).to.equal 200 - callback(null, body.invite) - -createProject = (owner, projectName, callback=(err, projectId, project)->) -> - owner.createProject projectName, (err, projectId) -> - throw err if err - fakeProject = { - _id: projectId, - name: projectName, - owner_ref: owner - } - callback(err, projectId, fakeProject) - -createProjectAndInvite = (owner, projectName, email, callback=(err, project, invite)->) -> - createProject owner, projectName, (err, projectId, project) -> - return callback(err) if err - createInvite owner, projectId, email, (err, invite) -> - return callback(err) if err - link = CollaboratorsEmailHandler._buildInviteUrl(project, invite) - callback(null, project, invite, link) - -revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) -> - sendingUser.getCsrfToken (err) -> - return callback(err) if err - sendingUser.request.delete { - uri: "/project/#{projectId}/invite/#{inviteId}", - }, (err, response, body) -> - return callback(err) if err - callback(null) - - -# Actions -tryFollowInviteLink = (user, link, callback=(err, response, body)->) -> - user.request.get { - uri: link - baseUrl: null - }, callback - -tryAcceptInvite = (user, invite, callback=(err, response, body)->) -> - user.request.post { - uri: "/project/#{invite.projectId}/invite/token/#{invite.token}/accept" - json: - token: invite.token - }, callback - -tryRegisterUser = (user, email, callback=(err, response, body)->) -> - user.getCsrfToken (error) => - return callback(error) if error? - user.request.post { - url: "/register" - json: - email: email - password: "some_weird_password" - }, callback - -tryFollowLoginLink = (user, loginLink, callback=(err, response, body)->) -> - user.getCsrfToken (error) => - return callback(error) if error? - user.request.get loginLink, callback - -tryLoginUser = (user, callback=(err, response, body)->) -> - user.getCsrfToken (error) => - return callback(error) if error? - user.request.post { - url: "/login" - json: - email: user.email - password: user.password - }, callback - -tryGetInviteList = (user, projectId, callback=(err, response, body)->) -> - user.getCsrfToken (error) => - return callback(error) if error? - user.request.get { - url: "/project/#{projectId}/invites" - json: true - }, callback - -tryJoinProject = (user, projectId, callback=(err, response, body)->) -> - user.getCsrfToken (error) => - return callback(error) if error? - user.request.post { - url: "/project/#{projectId}/join" - qs: {user_id: user._id} - auth: - user: settings.apis.web.user - pass: settings.apis.web.pass - sendImmediately: true - json: true - jar: false - }, callback - -# Expectations -expectProjectAccess = (user, projectId, callback=(err,result)->) -> - # should have access to project - user.openProject projectId, (err) => - expect(err).to.be.oneOf [null, undefined] - callback() - -expectNoProjectAccess = (user, projectId, callback=(err,result)->) -> - # should not have access to project page - user.openProject projectId, (err) => - expect(err).to.be.instanceof Error - callback() - -expectInvitePage = (user, link, callback=(err,result)->) -> - # view invite - tryFollowInviteLink user, link, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body).to.match new RegExp("Project Invite - .*") - callback() - -expectInvalidInvitePage = (user, link, callback=(err,result)->) -> - # view invalid invite - tryFollowInviteLink user, link, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body).to.match new RegExp("Invalid Invite - .*") - callback() - -expectInviteRedirectToRegister = (user, link, callback=(err,result)->) -> - # view invite, redirect to `/register` - tryFollowInviteLink user, link, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.match new RegExp("^/register.*$") - # follow redirect to register page and extract the redirectUrl from form - user.request.get response.headers.location, (err, response, body) -> - callback(null) - -expectLoginPage = (user, callback=(err, result)->) -> - tryFollowLoginLink user, "/login", (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body).to.match new RegExp("Login - .*") - callback(null) - -expectLoginRedirectToInvite = (user, link, callback=(err, result)->) -> - tryLoginUser user, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - callback(null, null) - -expectRegistrationRedirectToInvite = (user, email, link, callback=(err, result)->) -> - tryRegisterUser user, email, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - callback(null, null) - -expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) -> - # view invite, redirect straight to project - tryFollowInviteLink user, link, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/project/#{invite.projectId}" - callback() - -expectAcceptInviteAndRedirect = (user, invite, callback=(err,result)->) -> - # should accept the invite and redirect to project - tryAcceptInvite user, invite, (err, response, body) => - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/project/#{invite.projectId}" - callback() - -expectInviteListCount = (user, projectId, count, callback=(err)->) -> - tryGetInviteList user, projectId, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body).to.have.all.keys ['invites'] - expect(body.invites.length).to.equal count - callback() - -expectInvitesInJoinProjectCount = (user, projectId, count, callback=(err,result)->) -> - tryJoinProject user, projectId, (err, response, body) -> - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - expect(body.project).to.contain.keys ['invites'] - expect(body.project.invites.length).to.equal count - callback() - - - -describe "ProjectInviteTests", -> - before (done) -> - @sendingUser = new User() - @user = new User() - @site_admin = new User({email: "admin@example.com"}) - @email = 'smoketestuser@example.com' - @projectName = 'sharing test' - Async.series [ - (cb) => @user.ensureUserExists cb - (cb) => @sendingUser.login cb - (cb) => @sendingUser.setFeatures { collaborators: 10 }, cb - ], done - - describe 'creating invites', -> - - beforeEach (done) -> - @projectName = "wat" - @projectId = null - @fakeProject = null - done() - - describe 'creating two invites', -> - - beforeEach (done) -> - Async.series [ - (cb) => - createProject @sendingUser, @projectName, (err, projectId, project) => - @projectId = projectId - @fakeProject = project - cb() - ], done - - afterEach (done) -> - Async.series [ - (cb) => @sendingUser.deleteProject(@projectId, cb) - (cb) => @sendingUser.deleteProject(@projectId, cb) - ], done - - it 'should allow the project owner to create and remove invites', (done) -> - @invite = null - Async.series [ - (cb) => expectProjectAccess @sendingUser, @projectId, cb - (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb - # create invite, check invite list count - (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => - return cb(err) if err - @invite = invite - cb() - (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb - (cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb - (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb - # and a second time - (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => - return cb(err) if err - @invite = invite - cb() - (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb - # check the joinProject view - (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 1, cb - # revoke invite - (cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb - (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb - (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 0, cb - ], done - - it 'should allow the project owner to create many invites at once', (done) -> - @inviteOne = null - @inviteTwo = null - Async.series [ - (cb) => expectProjectAccess @sendingUser, @projectId, cb - (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb - # create first invite - (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => - return cb(err) if err - @inviteOne = invite - cb() - (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb - # and a second - (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) => - return cb(err) if err - @inviteTwo = invite - cb() - # should have two - (cb) => expectInviteListCount @sendingUser, @projectId, 2, cb - (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 2, cb - # revoke first - (cb) => revokeInvite @sendingUser, @projectId, @inviteOne._id, cb - (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb - # revoke second - (cb) => revokeInvite @sendingUser, @projectId, @inviteTwo._id, cb - (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb - ], done - - describe 'clicking the invite link', -> - - beforeEach (done) -> - @projectId = null - @fakeProject = null - done() - - - describe "user is logged in already", -> - - beforeEach (done) -> - Async.series [ - (cb) => - createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) => - @projectId = project._id - @fakeProject = project - @invite = invite - @link = link - cb() - (cb) => - @user.login (err) => - if err - throw err - cb() - ], done - - afterEach (done) -> - Async.series [ - (cb) => @sendingUser.deleteProject(@projectId, cb) - (cb) => @sendingUser.deleteProject(@projectId, cb) - (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) - ], done - - describe 'user is already a member of the project', -> - - beforeEach (done) -> - Async.series [ - (cb) => expectInvitePage @user, @link, cb - (cb) => expectAcceptInviteAndRedirect @user, @invite, cb - (cb) => expectProjectAccess @user, @invite.projectId, cb - ], done - - describe 'when user clicks on the invite a second time', -> - - it 'should just redirect to the project page', (done) -> - Async.series [ - (cb) => expectProjectAccess @user, @invite.projectId, cb - (cb) => expectInviteRedirectToProject @user, @link, @invite, cb - (cb) => expectProjectAccess @user, @invite.projectId, cb - ], done - - describe 'when the user recieves another invite to the same project', -> - - it 'should redirect to the project page', (done) -> - Async.series [ - (cb) => - createInvite @sendingUser, @projectId, @email, (err, invite) => - if err - throw err - @secondInvite = invite - @secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite) - cb() - (cb) => expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb - (cb) => expectProjectAccess @user, @invite.projectId, cb - (cb) => revokeInvite @sendingUser, @projectId, @secondInvite._id, cb - ], done - - - describe 'user is not a member of the project', -> - - it 'should not grant access if the user does not accept the invite', (done) -> - Async.series( - [ - (cb) => expectInvitePage @user, @link, cb - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done - ) - - it 'should render the invalid-invite page if the token is invalid', (done) -> - Async.series( - [ - (cb) => - link = @link.replace(@invite.token, 'not_a_real_token') - expectInvalidInvitePage @user, link, cb - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done - ) - - it 'should allow the user to accept the invite and access the project', (done) -> - Async.series( - [ - (cb) => expectInvitePage @user, @link, cb - (cb) => expectAcceptInviteAndRedirect @user, @invite, cb - (cb) => expectProjectAccess @user, @invite.projectId, cb - ], done - ) - - describe 'user is not logged in initially', -> - - before (done) -> - @user.logout done - - beforeEach (done) -> - Async.series [ - (cb) => - createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) => - @projectId = project._id - @fakeProject = project - @invite = invite - @link = link - cb() - ], done - - afterEach (done) -> - Async.series [ - (cb) => @sendingUser.deleteProject(@projectId, cb) - (cb) => @sendingUser.deleteProject(@projectId, cb) - (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb) - ], done - - describe 'registration prompt workflow with valid token', -> - - it 'should redirect to the register page', (done) -> - Async.series [ - (cb) => expectInviteRedirectToRegister(@user, @link, cb) - ], done - - it 'should allow user to accept the invite if the user registers a new account', (done) -> - Async.series [ - (cb) => expectInviteRedirectToRegister @user, @link, cb - (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @link, cb - (cb) => expectInvitePage @user, @link, cb - (cb) => expectAcceptInviteAndRedirect @user, @invite, cb - (cb) => expectProjectAccess @user, @invite.projectId, cb - ], done - - describe 'registration prompt workflow with non-valid token', -> - - before (done)-> - @user.logout done - - it 'should redirect to the register page', (done) -> - Async.series [ - (cb) => expectInviteRedirectToRegister(@user, @link, cb) - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done - - it 'should display invalid-invite if the user registers a new account', (done) -> - badLink = @link.replace(@invite.token, 'not_a_real_token') - Async.series [ - (cb) => expectInviteRedirectToRegister @user, badLink, cb - (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", badLink, cb - (cb) => expectInvalidInvitePage @user, badLink, cb - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done - - describe 'login workflow with valid token', -> - - before (done)-> - @user.logout done - - it 'should redirect to the register page', (done) -> - Async.series [ - (cb) => expectInviteRedirectToRegister(@user, @link, cb) - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done - - it 'should allow the user to login to view the invite', (done) -> - Async.series [ - (cb) => expectInviteRedirectToRegister @user, @link, cb - (cb) => expectLoginPage @user, cb - (cb) => expectLoginRedirectToInvite @user, @link, cb - (cb) => expectInvitePage @user, @link, cb - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done - - it 'should allow user to accept the invite if the user registers a new account', (done) -> - Async.series [ - (cb) => expectInvitePage @user, @link, cb - (cb) => expectAcceptInviteAndRedirect @user, @invite, cb - (cb) => expectProjectAccess @user, @invite.projectId, cb - ], done - - describe 'login workflow with non-valid token', -> - - before (done)-> - @user.logout done - - it 'should redirect to the register page', (done) -> - Async.series [ - (cb) => expectInviteRedirectToRegister(@user, @link, cb) - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done - - it 'should show the invalid-invite page once the user has logged in', (done) -> - badLink = @link.replace(@invite.token, 'not_a_real_token') - Async.series [ - (cb) => - expectInviteRedirectToRegister @user, badLink, cb - (cb) => - expectLoginPage @user, cb - (cb) => expectLoginRedirectToInvite @user, badLink, cb - (cb) => expectInvalidInvitePage @user, badLink, cb - (cb) => expectNoProjectAccess @user, @invite.projectId, cb - ], done diff --git a/services/web/test/acceptance/coffee/ProjectStructureMongoLockTest.coffee b/services/web/test/acceptance/coffee/ProjectStructureMongoLockTest.coffee deleted file mode 100644 index 7745a12ac5..0000000000 --- a/services/web/test/acceptance/coffee/ProjectStructureMongoLockTest.coffee +++ /dev/null @@ -1,88 +0,0 @@ -APP_PATH = "../../../app/js" - -LockManager = require "#{APP_PATH}/infrastructure/LockManager" -ProjectCreationHandler = require "#{APP_PATH}/Features/Project/ProjectCreationHandler.js" -ProjectGetter = require "#{APP_PATH}/Features/Project/ProjectGetter.js" -ProjectEntityMongoUpdateHandler = require "#{APP_PATH}/Features/Project/ProjectEntityMongoUpdateHandler.js" -UserCreator = require "#{APP_PATH}/Features/User/UserCreator.js" - -expect = require("chai").expect -_ = require("lodash") - -# These tests are neither acceptance tests nor unit tests. It's difficult to -# test/verify that our locking is doing what we hope. -# These tests call methods in ProjectGetter and ProjectEntityMongoUpdateHandler -# to see that they DO NOT work when a lock has been taken. -# -# It is tested that these methods DO work when the lock has not been taken in -# other acceptance tests. - -describe "ProjectStructureMongoLock", -> - describe "whilst a project lock is taken", -> - before (done) -> - # We want to instantly fail if the lock is taken - LockManager.MAX_LOCK_WAIT_TIME = 1 - @lockValue = "lock-value" - userDetails = - holdingAccount:false, - email: 'test@example.com' - UserCreator.createNewUser userDetails, (err, user) => - @user = user - throw err if err? - ProjectCreationHandler.createBlankProject user._id, 'locked-project', (err, project) => - throw err if err? - @locked_project = project - namespace = ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE - @lock_key = "lock:web:#{namespace}:#{project._id}" - LockManager._getLock @lock_key, namespace, (err, lockValue) => - @lockValue = lockValue - done() - return - - after (done) -> - LockManager._releaseLock @lock_key, @lockValue, done - - describe 'interacting with the locked project', -> - LOCKING_UPDATE_METHODS = ['addDoc', 'addFile', 'mkdirp', 'moveEntity', 'renameEntity', 'addFolder'] - for methodName in LOCKING_UPDATE_METHODS - it "cannot call ProjectEntityMongoUpdateHandler.#{methodName}", (done) -> - method = ProjectEntityMongoUpdateHandler[methodName] - args = _.times(method.length - 2, _.constant(null)) - method @locked_project._id, args, (err) -> - expect(err).to.deep.equal new Error("Timeout") - done() - - it "cannot get the project without a projection", (done) -> - ProjectGetter.getProject @locked_project._id, (err) -> - expect(err).to.deep.equal new Error("Timeout") - done() - - it "cannot get the project if rootFolder is in the projection", (done) -> - ProjectGetter.getProject @locked_project._id, rootFolder: true, (err) -> - expect(err).to.deep.equal new Error("Timeout") - done() - - it "can get the project if rootFolder is not in the projection", (done) -> - ProjectGetter.getProject @locked_project._id, _id: true, (err, project) => - expect(err).to.equal(null) - expect(project._id).to.deep.equal(@locked_project._id) - done() - - describe 'interacting with other projects', -> - before (done) -> - ProjectCreationHandler.createBlankProject @user._id, 'unlocked-project', (err, project) => - throw err if err? - @unlocked_project = project - done() - - it "can add folders to other projects", (done) -> - ProjectEntityMongoUpdateHandler.addFolder @unlocked_project._id, @unlocked_project.rootFolder[0]._id, 'new folder', (err, folder) -> - expect(err).to.equal(null) - expect(folder).to.be.defined - done() - - it "can get other projects without a projection", (done) -> - ProjectGetter.getProject @unlocked_project._id, (err, project) => - expect(err).to.equal(null) - expect(project._id).to.deep.equal(@unlocked_project._id) - done() diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee deleted file mode 100644 index cbd6825a93..0000000000 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ /dev/null @@ -1,845 +0,0 @@ -async = require "async" -expect = require("chai").expect -mkdirp = require "mkdirp" -ObjectId = require("mongojs").ObjectId -Path = require "path" -fs = require "fs" -Settings = require "settings-sharelatex" -_ = require "underscore" - -ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js" - -MockDocUpdaterApi = require './helpers/MockDocUpdaterApi' -MockFileStoreApi = require './helpers/MockFileStoreApi' -MockProjectHistoryApi = require './helpers/MockProjectHistoryApi' -request = require "./helpers/request" -User = require "./helpers/User" - -describe "ProjectStructureChanges", -> - example_project_id = null - example_doc_id = null - example_file_id = null - example_folder_id_1 = null - example_folder_id_2 = null - - before (done) -> - @owner = new User() - @owner.login done - - describe "creating a project from the example template", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - @owner.createProject "example-project", {template: "example"}, (error, project_id) => - throw error if error? - example_project_id = project_id - done() - - it "should version creating a doc", -> - {docUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(2) - _.each updates, (update) => - expect(update.userId).to.equal(@owner._id) - expect(update.docLines).to.be.a('string') - expect(_.where(updates, pathname: "/main.tex").length).to.equal 1 - expect(_.where(updates, pathname: "/references.bib").length).to.equal 1 - expect(version).to.equal(3) - - it "should version creating a file", -> - {fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/universe.jpg") - expect(update.url).to.be.a('string'); - expect(version).to.equal(3) - - describe "duplicating a project", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - @owner.request.post { - uri: "/Project/#{example_project_id}/clone", - json: - projectName: 'new.tex' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to add doc #{res.statusCode}") - @dup_project_id = body.project_id - done() - - it "should version the docs created", -> - {docUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id) - expect(updates.length).to.equal(2) - _.each updates, (update) => - expect(update.userId).to.equal(@owner._id) - expect(update.docLines).to.be.a('string') - expect(_.where(updates, pathname: "/main.tex").length).to.equal(1) - expect(_.where(updates, pathname: "/references.bib").length).to.equal(1) - expect(version).to.equal(3) - - it "should version the files created", -> - {fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/universe.jpg") - expect(update.url).to.be.a('string'); - expect(version).to.equal(3) - - describe "adding a doc", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - - ProjectGetter.getProject example_project_id, (error, project) => - throw error if error? - @project_0 = project - @owner.request.post { - uri: "project/#{example_project_id}/doc", - json: - name: 'new.tex' - parent_folder_id: project.rootFolder[0]._id - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to add doc #{res.statusCode}") - example_doc_id = body._id - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - done() - - it "should version the doc added", -> - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/new.tex") - expect(update.docLines).to.be.a('string'); - expect(version).to.equal(@project_0.version + 1) - - it "should increment the project structure version number", -> - expect(@project_1.version).to.equal(@project_0.version + 1) - - describe "uploading a project", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - - zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test_project.zip')) - @test_project_name = 'wombat' - - req = @owner.request.post { - uri: "project/new/upload", - formData: - qqfile: zip_file - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload project #{res.statusCode}") - @uploaded_project_id = JSON.parse(body).project_id - done() - - it "should version the docs created", -> - {docUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/main.tex") - expect(update.docLines).to.equal("Test") - expect(version).to.equal(2) - - it "should version the files created", -> - {fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - expect(update.url).to.be.a('string'); - expect(version).to.equal(2) - - describe "uploading a project with a name", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - - zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test_project_with_name.zip')) - @test_project_name = 'wombat' - - req = @owner.request.post { - uri: "project/new/upload", - formData: - qqfile: zip_file - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload project #{res.statusCode}") - @uploaded_project_id = JSON.parse(body).project_id - done() - - it "should set the project name from the zip contents", (done) -> - ProjectGetter.getProject @uploaded_project_id, (error, project) => - expect(error).not.to.exist - expect(project.name).to.equal @test_project_name - done() - - describe "uploading a project with an invalid name", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - - zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test_project_with_invalid_name.zip')) - @test_project_match = /^bad[^\\]+name$/ - - req = @owner.request.post { - uri: "project/new/upload", - formData: - qqfile: zip_file - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload project #{res.statusCode}") - @uploaded_project_id = JSON.parse(body).project_id - done() - - it "should set the project name from the zip contents", (done) -> - ProjectGetter.getProject @uploaded_project_id, (error, project) => - expect(error).not.to.exist - expect(project.name).to.match @test_project_match - done() - - describe "uploading a project with a shared top-level folder", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - - zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test_project_with_shared_top_level_folder.zip')) - - @owner.request.post { - uri: "project/new/upload", - formData: - qqfile: zip_file - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload project #{res.statusCode}") - @uploaded_project_id = JSON.parse(body).project_id - done() - - it "should not create the top-level folder", (done) -> - ProjectGetter.getProject @uploaded_project_id, (error, project) -> - expect(error).not.to.exist - expect(project.rootFolder[0].folders.length).to.equal 0 - expect(project.rootFolder[0].docs.length).to.equal 2 - done() - - describe "uploading a project with backslashes in the path names", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - - zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test_project_with_backslash_in_filename.zip')) - - @owner.request.post { - uri: "project/new/upload", - formData: - qqfile: zip_file - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload project #{res.statusCode}") - @uploaded_project_id = JSON.parse(body).project_id - done() - - it "should treat the backslash as a directory separator", (done) -> - ProjectGetter.getProject @uploaded_project_id, (error, project) -> - expect(error).not.to.exist - expect(project.rootFolder[0].folders[0].name).to.equal('styles') - expect(project.rootFolder[0].folders[0].docs[0].name).to.equal('ao.sty') - done() - - describe "uploading a project with files in different encodings", -> - before (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - - zip_file = fs.createReadStream(Path.resolve(__dirname + '/../files/charsets/charsets.zip')) - - @owner.request.post { - uri: "project/new/upload", - formData: - qqfile: zip_file - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload project #{res.statusCode}") - @uploaded_project_id = JSON.parse(body).project_id - done() - - it "should correctly parse windows-1252", -> - {docUpdates: updates} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id) - update = _.find updates, (update) -> - update.pathname == '/test-german-windows-1252.tex' - expect(update.docLines).to.contain("Der schnelle braune Fuchs sprang träge über den Hund.") - - it "should correctly parse German utf8", -> - {docUpdates: updates} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id) - update = _.find updates, (update) -> - update.pathname == '/test-german-utf8x.tex' - expect(update.docLines).to.contain("Der schnelle braune Fuchs sprang träge über den Hund.") - - it "should correctly parse little-endian utf16", -> - {docUpdates: updates} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id) - update = _.find updates, (update) -> - update.pathname == '/test-greek-utf16-le-bom.tex' - expect(update.docLines).to.contain("Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.") - - it "should correctly parse Greek utf8", -> - {docUpdates: updates} = MockDocUpdaterApi.getProjectStructureUpdates(@uploaded_project_id) - update = _.find updates, (update) -> - update.pathname == '/test-greek-utf8x.tex' - expect(update.docLines).to.contain("Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.") - - describe "uploading a file", -> - beforeEach (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - ProjectGetter.getProject example_project_id, (error, project) => - throw error if error? - @root_folder_id = project.rootFolder[0]._id.toString() - @project_0 = project - done() - - it "should version a newly uploaded file", (done) -> - image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/1pixel.png')) - - req = @owner.request.post { - uri: "project/#{example_project_id}/upload", - qs: - folder_id: @root_folder_id - formData: - qqfile: - value: image_file - options: - filename: '1pixel.png', - contentType: 'image/png' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload file #{res.statusCode}") - - example_file_id = JSON.parse(body).entity_id - - {fileUpdates: updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - expect(update.url).to.be.a('string'); - @original_file_url = update.url - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # uploading a new file does change the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - it "should version a replacement file", (done) -> - image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/2pixel.png')) - - req = @owner.request.post { - uri: "project/#{example_project_id}/upload", - qs: - folder_id: @root_folder_id - formData: - qqfile: - value: image_file - options: - filename: '1pixel.png', - contentType: 'image/png' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload file #{res.statusCode}") - - example_file_id = JSON.parse(body).entity_id - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(2) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - #expect(update.url).to.be.a('string'); - update = updates[1] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - expect(update.url).to.be.a('string'); - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - describe "moving entities", -> - before (done) -> - @owner.request.post { - uri: "project/#{example_project_id}/folder", - json: - name: 'foo' - }, (error, res, body) => - throw error if error? - example_folder_id_1 = body._id - done() - - beforeEach (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - ProjectGetter.getProject example_project_id, (error, project) => - throw error if error? - @root_folder_id = project.rootFolder[0]._id.toString() - @project_0 = project - done() - - it "should version moving a doc", (done) -> - @owner.request.post { - uri: "project/#{example_project_id}/Doc/#{example_doc_id}/move", - json: - folder_id: example_folder_id_1 - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to move doc #{res.statusCode}") - - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/new.tex") - expect(update.newPathname).to.equal("/foo/new.tex") - expect(version).to.equal(@project_0.version + 2) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 2) # 2 because it's a delete and then add - done() - - it "should version moving a file", (done) -> - @owner.request.post { - uri: "project/#{example_project_id}/File/#{example_file_id}/move", - json: - folder_id: example_folder_id_1 - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to move file #{res.statusCode}") - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - expect(update.newPathname).to.equal("/foo/1pixel.png") - expect(version).to.equal(@project_0.version + 2) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 2) # 2 because it's a delete and then add - done() - - it "should version moving a folder", (done) -> - @owner.request.post { - uri: "project/#{example_project_id}/folder", - json: - name: 'bar' - }, (error, res, body) => - throw error if error? - example_folder_id_2 = body._id - - @owner.request.post { - uri: "project/#{example_project_id}/Folder/#{example_folder_id_1}/move", - json: - folder_id: example_folder_id_2 - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to move folder #{res.statusCode}") - - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/foo/new.tex") - expect(update.newPathname).to.equal("/bar/foo/new.tex") - expect(version).to.equal(@project_0.version + 3) - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/foo/1pixel.png") - expect(update.newPathname).to.equal("/bar/foo/1pixel.png") - expect(version).to.equal(@project_0.version + 3) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 3) # because folder and 2 files move - done() - - describe "renaming entities", -> - beforeEach (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - ProjectGetter.getProject example_project_id, (error, project) => - throw error if error? - @root_folder_id = project.rootFolder[0]._id.toString() - @project_0 = project - done() - - it "should version renaming a doc", (done) -> - @owner.request.post { - uri: "project/#{example_project_id}/Doc/#{example_doc_id}/rename", - json: - name: 'new_renamed.tex' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to move doc #{res.statusCode}") - - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo/new.tex") - expect(update.newPathname).to.equal("/bar/foo/new_renamed.tex") - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - it "should version renaming a file", (done) -> - @owner.request.post { - uri: "project/#{example_project_id}/File/#{example_file_id}/rename", - json: - name: '1pixel_renamed.png' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to move file #{res.statusCode}") - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo/1pixel.png") - expect(update.newPathname).to.equal("/bar/foo/1pixel_renamed.png") - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - it "should version renaming a folder", (done) -> - @owner.request.post { - uri: "project/#{example_project_id}/Folder/#{example_folder_id_1}/rename", - json: - name: 'foo_renamed' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to move folder #{res.statusCode}") - - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo/new_renamed.tex") - expect(update.newPathname).to.equal("/bar/foo_renamed/new_renamed.tex") - expect(version).to.equal(@project_0.version + 1) - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo/1pixel_renamed.png") - expect(update.newPathname).to.equal("/bar/foo_renamed/1pixel_renamed.png") - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - - describe "deleting entities", -> - beforeEach (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - ProjectGetter.getProject example_project_id, (error, project) => - throw error if error? - @root_folder_id = project.rootFolder[0]._id.toString() - @project_0 = project - done() - - it "should version deleting a folder", (done) -> - @owner.request.delete { - uri: "project/#{example_project_id}/Folder/#{example_folder_id_2}", - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to delete folder #{res.statusCode}") - - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo_renamed/new_renamed.tex") - expect(update.newPathname).to.equal("") - expect(version).to.equal(@project_0.version + 1) - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo_renamed/1pixel_renamed.png") - expect(update.newPathname).to.equal("") - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject example_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - describe "tpds", -> - before (done) -> - @tpds_project_name = "tpds-project-#{new ObjectId().toString()}" - @owner.createProject @tpds_project_name, (error, project_id) => - throw error if error? - @tpds_project_id = project_id - mkdirp Settings.path.dumpFolder, done - - beforeEach (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - ProjectGetter.getProject @tpds_project_id, (error, project) => - throw error if error? - @root_folder_id = project.rootFolder[0]._id.toString() - @project_0 = project - done() - - it "should version adding a doc", (done) -> - tex_file = fs.createReadStream(Path.resolve(__dirname + '/../files/test.tex')) - - req = @owner.request.post { - uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex", - auth: - user: _.keys(Settings.httpAuthUsers)[0] - pass: _.values(Settings.httpAuthUsers)[0] - sendImmediately: true - } - - tex_file.on "error", (err) -> - throw err - - req.on "error", (err) -> - throw err - - req.on "response", (res) => - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload file #{res.statusCode}") - - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/test.tex") - expect(update.docLines).to.equal("Test") - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject @tpds_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - tex_file.pipe(req) - - it "should version adding a new file", (done) -> - image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/1pixel.png')) - - req = @owner.request.post { - uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/1pixel.png", - auth: - user: _.keys(Settings.httpAuthUsers)[0] - pass: _.values(Settings.httpAuthUsers)[0] - sendImmediately: true - } - - image_file.on "error", (err) -> - throw err - - req.on "error", (err) -> - throw err - - req.on "response", (res) => - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload file #{res.statusCode}") - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - expect(update.url).to.be.a('string'); - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject @tpds_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - image_file.pipe(req) - - it "should version replacing a file", (done) -> - image_file = fs.createReadStream(Path.resolve(__dirname + '/../files/2pixel.png')) - - req = @owner.request.post { - uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/1pixel.png", - auth: - user: _.keys(Settings.httpAuthUsers)[0] - pass: _.values(Settings.httpAuthUsers)[0] - sendImmediately: true - } - - image_file.on "error", (err) -> - throw err - - req.on "error", (err) -> - throw err - - req.on "response", (res) => - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload file #{res.statusCode}") - - {fileUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id) - expect(updates.length).to.equal(2) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - #expect(update.url).to.be.a('string'); - update = updates[1] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/1pixel.png") - expect(update.url).to.be.a('string'); - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject @tpds_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - image_file.pipe(req) - - it "should version deleting a doc", (done) -> - req = @owner.request.delete { - uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex", - auth: - user: _.keys(Settings.httpAuthUsers)[0] - pass: _.values(Settings.httpAuthUsers)[0] - sendImmediately: true - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to delete doc #{res.statusCode}") - - {docUpdates:updates, version} = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id) - expect(updates.length).to.equal(1) - update = updates[0] - expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/test.tex") - expect(update.newPathname).to.equal("") - expect(version).to.equal(@project_0.version + 1) - - ProjectGetter.getProject @tpds_project_id, (error, newProject) => - throw error if error? - @project_1 = newProject - # replacing a file should update the project structure - expect(@project_1.version).to.equal(@project_0.version + 1) - done() - - - describe "uploading a document", -> - beforeEach (done) -> - MockDocUpdaterApi.clearProjectStructureUpdates() - ProjectGetter.getProject example_project_id, (error, project) => - throw error if error? - @root_folder_id = project.rootFolder[0]._id.toString() - @project_0 = project - done() - - describe "with an unusual character set", -> - it "should correctly handle utf16-le data", (done) -> - document_file = fs.createReadStream(Path.resolve(__dirname + '/../files/charsets/test-greek-utf16-le-bom.tex')) - - req = @owner.request.post { - uri: "project/#{example_project_id}/upload", - qs: - folder_id: @root_folder_id - formData: - qqfile: - value: document_file - options: - filename: 'test-greek-utf16-le-bom.tex', - contentType: 'text/x-tex' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload file #{res.statusCode}") - - example_file_id = JSON.parse(body).entity_id - - {docUpdates:updates} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - update = updates[0] - expect(update.pathname).to.equal('/test-greek-utf16-le-bom.tex') - expect(update.docLines).to.contain("Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.") - done() - - it "should correctly handle windows1252/iso-8859-1/latin1 data", (done) -> - document_file = fs.createReadStream(Path.resolve(__dirname + '/../files/charsets/test-german-windows-1252.tex')) - - req = @owner.request.post { - uri: "project/#{example_project_id}/upload", - qs: - folder_id: @root_folder_id - formData: - qqfile: - value: document_file - options: - filename: 'test-german-windows-1252.tex', - contentType: 'text/x-tex' - }, (error, res, body) => - throw error if error? - if res.statusCode < 200 || res.statusCode >= 300 - throw new Error("failed to upload file #{res.statusCode}") - - example_file_id = JSON.parse(body).entity_id - - {docUpdates:updates} = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) - update = updates[0] - expect(update.pathname).to.equal('/test-german-windows-1252.tex') - expect(update.docLines).to.contain("Der schnelle braune Fuchs sprang träge über den Hund.") - done() diff --git a/services/web/test/acceptance/coffee/ProxyUrls.coffee b/services/web/test/acceptance/coffee/ProxyUrls.coffee deleted file mode 100644 index c7fac25b1e..0000000000 --- a/services/web/test/acceptance/coffee/ProxyUrls.coffee +++ /dev/null @@ -1,42 +0,0 @@ -should = require('chai').should() -assert = require('chai').assert -async = require("async") -request = require "./helpers/request" -MockV1Api = require "./helpers/MockV1Api" - -assertResponse = (path, expectedStatusCode, expectedBody, cb) -> - request.get path, (error, response) -> - should.not.exist error - response.statusCode.should.equal expectedStatusCode - assert.deepEqual(JSON.parse(response.body), expectedBody) if expectedBody - cb() - -describe "ProxyUrls", -> - before -> - @timeout(1000) - - it 'proxy static URLs', (done) -> - async.series [ - (cb) -> assertResponse '/institutions/list', 200, [], cb - (cb) -> assertResponse '/institutions/domains', 200, [], cb - ], - done - - it 'proxy dynamic URLs', (done) -> - async.series [ - (cb) -> assertResponse '/institutions/list/123', 200, { id: 123, name: "Institution 123" }, cb - (cb) -> assertResponse '/institutions/list/456', 200, { id: 456, name: "Institution 456" }, cb - ], - done - - it 'return 404 if proxy is not set', (done) -> - async.series [ - (cb) -> assertResponse '/institutions/foobar', 404, null, cb - ], - done - - it 'handle missing baseUrl', (done) -> - async.series [ - (cb) -> assertResponse '/proxy/missing/baseUrl', 500, null, cb - ], - done diff --git a/services/web/test/acceptance/coffee/RedirectUrlsTests.coffee b/services/web/test/acceptance/coffee/RedirectUrlsTests.coffee deleted file mode 100644 index e6ed784962..0000000000 --- a/services/web/test/acceptance/coffee/RedirectUrlsTests.coffee +++ /dev/null @@ -1,49 +0,0 @@ -should = require('chai').should() -assert = require('chai').assert -async = require("async") -request = require "./helpers/request" -MockV1Api = require "./helpers/MockV1Api" - -assertRedirect = (method, path, expectedStatusCode, destination, cb) -> - request[method] path, (error, response) -> - should.not.exist error - response.statusCode.should.equal expectedStatusCode - response.headers.location.should.equal destination - cb() - -describe "RedirectUrls", -> - before -> - @timeout(1000) - - it 'proxy static URLs', (done) -> - assertRedirect 'get', '/redirect/one', 302, '/destination/one', done - - it 'proxy dynamic URLs', (done) -> - assertRedirect 'get', '/redirect/params/42', 302, '/destination/42/params', done - - it 'proxy URLs with baseUrl', (done) -> - assertRedirect 'get', '/redirect/base_url', 302, 'https://example.com/destination/base_url', done - - it 'proxy URLs with POST with a 307', (done) -> - assertRedirect 'post', '/redirect/get_and_post', 307, '/destination/get_and_post', done - - it 'proxy URLs with multiple support methods', (done) -> - assertRedirect 'get', '/redirect/get_and_post', 302, '/destination/get_and_post', done - - it 'redirects with query params', (done) -> - assertRedirect 'get', '/redirect/qs?foo=bar&baz[]=qux1&baz[]=qux2', 302, '/destination/qs?foo=bar&baz[]=qux1&baz[]=qux2', done - - it "skips redirects if the 'skip-redirects' header is set", (done) -> - request.get {url: '/redirect/one', headers: {'x-skip-redirects': 'true'}}, (error, response) -> - should.not.exist error - response.statusCode.should.equal 404 - done() - - it 'redirects to /sign_in_to_v1 with authWithV1 setting', (done) -> - assertRedirect( - 'get', - '/docs_v1?zip_uri=http%3A%2F%2Foverleaf.test%2Ffoo%3Fbar%3Dbaz%26qux%3Dthing&bar=baz', - 302, - '/sign_in_to_v1?return_to=%2Fdocs%3Fzip_uri%3Dhttp%253A%252F%252Foverleaf.test%252Ffoo%253Fbar%253Dbaz%2526qux%253Dthing%26bar%3Dbaz', - done - ) diff --git a/services/web/test/acceptance/coffee/RegistrationTests.coffee b/services/web/test/acceptance/coffee/RegistrationTests.coffee deleted file mode 100644 index 09b93fe367..0000000000 --- a/services/web/test/acceptance/coffee/RegistrationTests.coffee +++ /dev/null @@ -1,214 +0,0 @@ -expect = require("chai").expect -assert = require("chai").assert -async = require("async") -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" -redis = require "./helpers/redis" -_ = require 'lodash' - -# Currently this is testing registration via the 'public-registration' module, -# whereas in production we're using the 'overleaf-integration' module. - -# Expectations -expectProjectAccess = (user, projectId, callback=(err,result)->) -> - # should have access to project - user.openProject projectId, (err) => - expect(err).to.be.oneOf [null, undefined] - callback() - -expectNoProjectAccess = (user, projectId, callback=(err,result)->) -> - # should not have access to project page - user.openProject projectId, (err) => - expect(err).to.be.instanceof Error - callback() - -# Actions -tryLoginThroughRegistrationForm = (user, email, password, callback=(err, response, body)->) -> - user.getCsrfToken (err) -> - return callback(err) if err? - user.request.post { - url: "/register" - json: - email: email - password: password - }, callback - - -describe "LoginRateLimit", -> - - before -> - @user = new User() - @badEmail = 'bademail@example.com' - @badPassword = 'badpassword' - - it 'should rate limit login attempts after 10 within two minutes', (done) -> - @user.request.get '/login', (err, res, body) => - async.timesSeries( - 15 - , (n, cb) => - @user.getCsrfToken (error) => - return cb(error) if error? - @user.request.post { - url: "/login" - json: - email: @badEmail - password: @badPassword - }, (err, response, body) => - cb(null, body?.message?.text) - , (err, results) => - # ten incorrect-credentials messages, then five rate-limit messages - expect(results.length).to.equal 15 - assert.deepEqual( - results, - _.concat( - _.fill([1..10], 'Your email or password is incorrect. Please try again'), - _.fill([1..5], 'This account has had too many login requests. Please wait 2 minutes before trying to log in again') - ) - ) - done() - ) - - -describe "CSRF protection", -> - - beforeEach -> - @user = new User() - @email = "test+#{Math.random()}@example.com" - @password = "password11" - - afterEach -> - @user.full_delete_user(@email) - - it 'should register with the csrf token', (done) -> - @user.request.get '/login', (err, res, body) => - @user.getCsrfToken (error) => - @user.request.post { - url: "/register" - json: - email: @email - password: @password - headers:{ - "x-csrf-token": @user.csrfToken - } - }, (error, response, body) => - expect(err?).to.equal false - expect(response.statusCode).to.equal 200 - done() - - it 'should fail with no csrf token', (done) -> - @user.request.get '/login', (err, res, body) => - @user.getCsrfToken (error) => - @user.request.post { - url: "/register" - json: - email: @email - password: @password - headers:{ - "x-csrf-token": "" - } - }, (error, response, body) => - expect(response.statusCode).to.equal 403 - done() - - it 'should fail with a stale csrf token', (done) -> - @user.request.get '/login', (err, res, body) => - @user.getCsrfToken (error) => - oldCsrfToken = @user.csrfToken - @user.logout (err) => - @user.request.post { - url: "/register" - json: - email: @email - password: @password - headers:{ - "x-csrf-token": oldCsrfToken - } - }, (error, response, body) => - expect(response.statusCode).to.equal 403 - done() - -describe "Register", -> - before -> - @user = new User() - - it 'Set emails attribute', (done) -> - @user.register (error, user) => - expect(error).to.not.exist - user.email.should.equal @user.email - user.emails.should.exist - user.emails.should.be.a 'array' - user.emails.length.should.equal 1 - user.emails[0].email.should.equal @user.email - done() - -describe "Register with bonus referal id", -> - before (done) -> - @user1 = new User() - @user2 = new User() - async.series [ - (cb) => @user1.register cb - (cb) => @user2.registerWithQuery '?r=' + @user1.referal_id + '&rm=d&rs=b', cb - ], done - - it 'Adds a referal when an id is supplied and the referal source is "bonus"', (done) -> - @user1.get (error, user) => - expect(error).to.not.exist - user.refered_user_count.should.eql 1 - - done() - -describe "LoginViaRegistration", -> - - before (done) -> - @timeout(60000) - @user1 = new User() - @user2 = new User() - async.series [ - (cb) => @user1.login cb - (cb) => @user1.logout cb - (cb) => redis.clearUserSessions @user1, cb - (cb) => @user2.login cb - (cb) => @user2.logout cb - (cb) => redis.clearUserSessions @user2, cb - ], done - @project_id = null - - describe "[Security] Trying to register/login as another user", -> - - it 'should not allow sign in with secondary email', (done) -> - secondaryEmail = "acceptance-test-secondary@example.com" - @user1.addEmail secondaryEmail, (err) => - @user1.loginWith secondaryEmail, (err) => - expect(err?).to.equal false - @user1.isLoggedIn (err, isLoggedIn) -> - expect(isLoggedIn).to.equal false - done() - - it 'should have user1 login', (done) -> - @user1.login (err) -> - expect(err?).to.equal false - done() - - it 'should have user1 create a project', (done) -> - @user1.createProject 'Private Project', (err, project_id) => - expect(err?).to.equal false - @project_id = project_id - done() - - it 'should ensure user1 can access their project', (done) -> - expectProjectAccess @user1, @project_id, done - - it 'should ensure user2 cannot access the project', (done) -> - expectNoProjectAccess @user2, @project_id, done - - it 'should prevent user2 from login/register with user1 email address', (done) -> - tryLoginThroughRegistrationForm @user2, @user1.email, 'totally_not_the_right_password', (err, response, body) => - expect(body.redir?).to.equal false - expect(body.message?).to.equal true - expect(body.message).to.have.all.keys('type', 'text') - expect(body.message.type).to.equal 'error' - done() - - it 'should still ensure user2 cannot access the project', (done) -> - expectNoProjectAccess @user2, @project_id, done diff --git a/services/web/test/acceptance/coffee/RestoringFilesTest.coffee b/services/web/test/acceptance/coffee/RestoringFilesTest.coffee deleted file mode 100644 index 14990be480..0000000000 --- a/services/web/test/acceptance/coffee/RestoringFilesTest.coffee +++ /dev/null @@ -1,197 +0,0 @@ -async = require "async" -expect = require("chai").expect -_ = require 'underscore' -fs = require 'fs' -Path = require 'path' - -ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js" - -User = require "./helpers/User" -MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi" -MockDocstoreApi = require "./helpers/MockDocstoreApi" -MockFileStoreApi = require "./helpers/MockFileStoreApi" - -describe "RestoringFiles", -> - before (done) -> - @owner = new User() - @owner.login (error) => - throw error if error? - @owner.createProject "example-project", {template: "example"}, (error, @project_id) => - throw error if error? - done() - - describe "restoring a deleted doc", -> - beforeEach (done) -> - @owner.getProject @project_id, (error, project) => - throw error if error? - @doc = _.find project.rootFolder[0].docs, (doc) -> - doc.name == 'main.tex' - @owner.request { - method: "DELETE", - url: "/project/#{@project_id}/doc/#{@doc._id}", - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 204 - @owner.request { - method: "POST", - url: "/project/#{@project_id}/doc/#{@doc._id}/restore" - json: - name: "main.tex" - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - expect(body.doc_id).to.exist - @restored_doc_id = body.doc_id - done() - - it 'should have restored the doc', (done) -> - @owner.getProject @project_id, (error, project) => - throw error if error? - restored_doc = _.find project.rootFolder[0].docs, (doc) -> - doc.name == 'main.tex' - expect(restored_doc._id.toString()).to.equal @restored_doc_id - expect(@doc._id).to.not.equal @restored_doc_id - # console.log @doc_id, @restored_doc_id, MockDocstoreApi.docs[@project_id] - expect(MockDocstoreApi.docs[@project_id][@restored_doc_id].lines).to.deep.equal( - MockDocstoreApi.docs[@project_id][@doc._id].lines - ) - done() - - describe "restoring from v2 history", -> - describe "restoring a text file", -> - beforeEach (done) -> - MockProjectHistoryApi.addOldFile(@project_id, 42, "foo.tex", "hello world, this is foo.tex!") - @owner.request { - method: "POST", - url: "/project/#{@project_id}/restore_file", - json: - pathname: "foo.tex" - version: 42 - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - done() - - it "should have created a doc", (done) -> - @owner.getProject @project_id, (error, project) => - throw error if error? - doc = _.find project.rootFolder[0].docs, (doc) -> - doc.name == 'foo.tex' - doc = MockDocstoreApi.docs[@project_id][doc._id] - expect(doc.lines).to.deep.equal [ - "hello world, this is foo.tex!" - ] - done() - - describe "restoring a binary file", -> - beforeEach (done) -> - @pngData = fs.readFileSync(Path.resolve(__dirname, '../files/1pixel.png'), 'binary') - MockProjectHistoryApi.addOldFile(@project_id, 42, "image.png", @pngData) - @owner.request { - method: "POST", - url: "/project/#{@project_id}/restore_file", - json: - pathname: "image.png" - version: 42 - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - done() - - it "should have created a file", (done) -> - @owner.getProject @project_id, (error, project) => - throw error if error? - file = _.find project.rootFolder[0].fileRefs, (file) -> - file.name == 'image.png' - file = MockFileStoreApi.files[@project_id][file._id] - expect(file.content).to.equal @pngData - done() - - describe "restoring to a directory that exists", -> - beforeEach (done) -> - MockProjectHistoryApi.addOldFile(@project_id, 42, "foldername/foo2.tex", "hello world, this is foo-2.tex!") - @owner.request.post { - uri: "project/#{@project_id}/folder", - json: - name: 'foldername' - }, (error, response, body) => - throw error if error? - expect(response.statusCode).to.equal 200 - @owner.request { - method: "POST", - url: "/project/#{@project_id}/restore_file", - json: - pathname: "foldername/foo2.tex" - version: 42 - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - done() - - it "should have created the doc in the named folder", (done) -> - @owner.getProject @project_id, (error, project) => - throw error if error? - folder = _.find project.rootFolder[0].folders, (folder) -> - folder.name == 'foldername' - doc = _.find folder.docs, (doc) -> - doc.name == 'foo2.tex' - doc = MockDocstoreApi.docs[@project_id][doc._id] - expect(doc.lines).to.deep.equal [ - "hello world, this is foo-2.tex!" - ] - done() - - describe "restoring to a directory that no longer exists", -> - beforeEach (done) -> - MockProjectHistoryApi.addOldFile(@project_id, 42, "nothere/foo3.tex", "hello world, this is foo-3.tex!") - @owner.request { - method: "POST", - url: "/project/#{@project_id}/restore_file", - json: - pathname: "nothere/foo3.tex" - version: 42 - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - done() - - it "should have created the folder and restored the doc to it", (done) -> - @owner.getProject @project_id, (error, project) => - throw error if error? - folder = _.find project.rootFolder[0].folders, (folder) -> - folder.name == 'nothere' - expect(folder).to.exist - doc = _.find folder.docs, (doc) -> - doc.name == 'foo3.tex' - doc = MockDocstoreApi.docs[@project_id][doc._id] - expect(doc.lines).to.deep.equal [ - "hello world, this is foo-3.tex!" - ] - done() - - describe "restoring to a filename that already exists", -> - beforeEach (done) -> - MockProjectHistoryApi.addOldFile(@project_id, 42, "main.tex", "hello world, this is main.tex!") - @owner.request { - method: "POST", - url: "/project/#{@project_id}/restore_file", - json: - pathname: "main.tex" - version: 42 - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - done() - - it "should have created the doc in the root folder", (done) -> - @owner.getProject @project_id, (error, project) => - throw error if error? - doc = _.find project.rootFolder[0].docs, (doc) -> - doc.name.match(/main \(Restored on/) - expect(doc).to.exist - doc = MockDocstoreApi.docs[@project_id][doc._id] - expect(doc.lines).to.deep.equal [ - "hello world, this is main.tex!" - ] - done() - diff --git a/services/web/test/acceptance/coffee/SecurityHeadersTests.coffee b/services/web/test/acceptance/coffee/SecurityHeadersTests.coffee deleted file mode 100644 index f3fddf4b07..0000000000 --- a/services/web/test/acceptance/coffee/SecurityHeadersTests.coffee +++ /dev/null @@ -1,67 +0,0 @@ -assert = require('chai').assert -async = require('async') -User = require('./helpers/User') -request = require('./helpers/request') - -assert_has_common_headers = (response) -> - headers = response.headers - assert.equal(headers['x-download-options'], 'noopen') - assert.equal(headers['x-xss-protection'], '1; mode=block') - assert.equal(headers['referrer-policy'], 'origin-when-cross-origin') - -assert_has_cache_headers = (response) -> - headers = response.headers - assert.equal(headers['surrogate-control'], 'no-store') - assert.equal(headers['cache-control'], 'no-store, no-cache, must-revalidate, proxy-revalidate') - assert.equal(headers['pragma'], 'no-cache') - assert.equal(headers['expires'], '0') - -assert_has_no_cache_headers = (response) -> - headers = response.headers - assert.isUndefined(headers['surrogate-control']) - assert.isUndefined(headers['cache-control']) - assert.isUndefined(headers['pragma']) - assert.isUndefined(headers['expires']) - -describe "SecurityHeaders", -> - before -> - @user = new User() - - it 'should not have x-powered-by header', (done) -> - request.get '/', (err, res, body) => - assert.isUndefined(res.headers['x-powered-by']) - done() - - it 'should have all common headers', (done) -> - request.get '/', (err, res, body) => - assert_has_common_headers res - done() - - it 'should not have cache headers on public pages', (done) -> - request.get '/', (err, res, body) => - assert_has_no_cache_headers res - done() - - it 'should have cache headers when user is logged in', (done) -> - async.series [ - (cb) => @user.login cb - (cb) => @user.request.get '/', cb - (cb) => @user.logout cb - ], (err, results) => - main_response = results[1][0] - assert_has_cache_headers main_response - done() - - it 'should have cache headers on project page', (done) -> - async.series [ - (cb) => @user.login cb - (cb) => - @user.createProject "public-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - @user.makePublic @project_id, "readAndWrite", cb - (cb) => @user.logout cb - ], (err, results) => - request.get "/project/#{@project_id}", (err, res, body) => - assert_has_cache_headers res - done() diff --git a/services/web/test/acceptance/coffee/SessionTests.coffee b/services/web/test/acceptance/coffee/SessionTests.coffee deleted file mode 100644 index fb628c0c0c..0000000000 --- a/services/web/test/acceptance/coffee/SessionTests.coffee +++ /dev/null @@ -1,382 +0,0 @@ -expect = require("chai").expect -async = require("async") -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" -redis = require "./helpers/redis" -MockV1Api = require './helpers/MockV1Api' - -describe "Sessions", -> - before (done) -> - @timeout(20000) - @user1 = new User() - @site_admin = new User({email: "admin@example.com"}) - async.series [ - (cb) => @user1.login cb - (cb) => @user1.logout cb - ], done - - describe "one session", -> - - it "should have one session in UserSessions set", (done) -> - async.series( - [ - (next) => - redis.clearUserSessions @user1, next - - # login, should add session to set - , (next) => - @user1.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 1 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - next() - - # should be able to access project list page - , (next) => - @user1.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 200 - next() - - # logout, should remove session from set - , (next) => - @user1.logout (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 0 - next() - - ], (err, result) => - if err - throw err - done() - ) - - describe "two sessions", -> - - before -> - # set up second session for this user - @user2 = new User() - @user2.email = @user1.email - @user2.password = @user1.password - - it "should have two sessions in UserSessions set", (done) -> - async.series( - [ - (next) => - redis.clearUserSessions @user1, next - - # login, should add session to set - , (next) => - @user1.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 1 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - next() - - # login again, should add the second session to set - , (next) => - @user2.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 2 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - expect(sessions[1].slice(0, 5)).to.equal 'sess:' - next() - - # both should be able to access project list page - , (next) => - @user1.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 200 - next() - - , (next) => - @user2.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 200 - next() - - # logout first session, should remove session from set - , (next) => - @user1.logout (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 1 - next() - - # first session should not have access to project list page - , (next) => - @user1.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 302 - next() - - # second session should still have access to settings - , (next) => - @user2.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 200 - next() - - # logout second session, should remove last session from set - , (next) => - @user2.logout (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 0 - next() - - # second session should not have access to project list page - , (next) => - @user2.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 302 - next() - - ], (err, result) => - if err - throw err - done() - ) - - describe 'three sessions, password reset', -> - - before -> - # set up second session for this user - @user2 = new User() - @user2.email = @user1.email - @user2.password = @user1.password - @user3 = new User() - @user3.email = @user1.email - @user3.password = @user1.password - - it "should erase both sessions when password is reset", (done) -> - async.series( - [ - (next) => - redis.clearUserSessions @user1, next - - # login, should add session to set - , (next) => - @user1.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 1 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - next() - - # login again, should add the second session to set - , (next) => - @user2.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 2 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - expect(sessions[1].slice(0, 5)).to.equal 'sess:' - next() - - # login third session, should add the second session to set - , (next) => - @user3.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 3 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - expect(sessions[1].slice(0, 5)).to.equal 'sess:' - next() - - # password reset from second session, should erase two of the three sessions - , (next) => - @user2.changePassword (err) -> - next(err) - - , (next) => - redis.getUserSessions @user2, (err, sessions) => - expect(sessions.length).to.equal 1 - next() - - # users one and three should not be able to access project list page - , (next) => - @user1.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 302 - next() - - , (next) => - @user3.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 302 - next() - - # user two should still be logged in, and able to access project list page - , (next) => - @user2.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 200 - next() - - # logout second session, should remove last session from set - , (next) => - @user2.logout (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 0 - next() - - ], (err, result) => - if err - throw err - done() - ) - - describe 'three sessions, sessions page', -> - - before (done) -> - # set up second session for this user - @user2 = new User() - @user2.email = @user1.email - @user2.password = @user1.password - @user3 = new User() - @user3.email = @user1.email - @user3.password = @user1.password - async.series [ - @user2.login.bind(@user2) - @user2.activateSudoMode.bind(@user2) - ], done - - it "should allow the user to erase the other two sessions", (done) -> - async.series( - [ - (next) => - redis.clearUserSessions @user1, next - - # login, should add session to set - , (next) => - @user1.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 1 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - next() - - # login again, should add the second session to set - , (next) => - @user2.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 2 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - expect(sessions[1].slice(0, 5)).to.equal 'sess:' - next() - - # login third session, should add the second session to set - , (next) => - @user3.login (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 3 - expect(sessions[0].slice(0, 5)).to.equal 'sess:' - expect(sessions[1].slice(0, 5)).to.equal 'sess:' - next() - - # enter sudo-mode - , (next) => - @user2.getCsrfToken (err) => - expect(err).to.be.oneOf [null, undefined] - @user2.request.post { - uri: '/confirm-password', - json: - password: @user2.password - }, (err, response, body) => - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - next() - - # check the sessions page - , (next) => - @user2.request.get { - uri: '/user/sessions' - }, (err, response, body) => - expect(err).to.be.oneOf [null, undefined] - expect(response.statusCode).to.equal 200 - next() - - # clear sessions from second session, should erase two of the three sessions - , (next) => - @user2.getCsrfToken (err) => - expect(err).to.be.oneOf [null, undefined] - @user2.request.post { - uri: '/user/sessions/clear' - }, (err) -> - next(err) - - , (next) => - redis.getUserSessions @user2, (err, sessions) => - expect(sessions.length).to.equal 1 - next() - - # users one and three should not be able to access project list page - , (next) => - @user1.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 302 - next() - - , (next) => - @user3.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 302 - next() - - # user two should still be logged in, and able to access project list page - , (next) => - @user2.getProjectListPage (err, statusCode) => - expect(err).to.equal null - expect(statusCode).to.equal 200 - next() - - # logout second session, should remove last session from set - , (next) => - @user2.logout (err) -> - next(err) - - , (next) => - redis.getUserSessions @user1, (err, sessions) => - expect(sessions.length).to.equal 0 - next() - - ], (err, result) => - if err - throw err - done() - ) diff --git a/services/web/test/acceptance/coffee/SettingsTests.coffee b/services/web/test/acceptance/coffee/SettingsTests.coffee deleted file mode 100644 index 78349b0952..0000000000 --- a/services/web/test/acceptance/coffee/SettingsTests.coffee +++ /dev/null @@ -1,41 +0,0 @@ -should = require('chai').should() -async = require("async") -User = require "./helpers/User" -MockV1Api = require './helpers/MockV1Api' - -describe 'SettingsPage', -> - - before (done) -> - @user = new User() - @v1Id = 1234 - @v1User = - id: @v1Id - email: @user.email - password: @user.password - profile: - id: @v1Id - email: @user.email - async.series [ - @user.ensureUserExists.bind(@user) - @user.login.bind(@user) - (cb) => @user.mongoUpdate {$set: {'overleaf.id': @v1Id}}, cb - (cb) => - MockV1Api.setUser @v1Id, @v1User - cb() - @user.activateSudoMode.bind(@user) - ], done - - it 'load settings page', (done) -> - @user.getUserSettingsPage (err, statusCode) -> - statusCode.should.equal 200 - done() - - it 'update main email address', (done) -> - newEmail = 'foo@bar.com' - @user.updateSettings email: newEmail, (error) => - should.not.exist error - @user.get (error, user) -> - user.email.should.equal newEmail - user.emails.length.should.equal 1 - user.emails[0].email.should.equal newEmail - done() diff --git a/services/web/test/acceptance/coffee/SubscriptionTests.coffee b/services/web/test/acceptance/coffee/SubscriptionTests.coffee deleted file mode 100644 index 011af90f35..0000000000 --- a/services/web/test/acceptance/coffee/SubscriptionTests.coffee +++ /dev/null @@ -1,354 +0,0 @@ -expect = require('chai').expect -async = require("async") -User = require "./helpers/User" -{Subscription} = require "../../../app/js/models/Subscription" -{Institution} = require "../../../app/js/models/Institution" -SubscriptionViewModelBuilder = require "../../../app/js/Features/Subscription/SubscriptionViewModelBuilder" - -MockRecurlyApi = require "./helpers/MockRecurlyApi" -MockV1Api = require "./helpers/MockV1Api" - -describe 'Subscriptions', -> - describe 'dashboard', -> - before (done) -> - @user = new User() - @user.ensureUserExists done - - describe 'when the user has no subscription', -> - before (done) -> - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - - it 'should return no personalSubscription', -> - expect(@data.personalSubscription).to.equal null - - it 'should return no memberGroupSubscriptions', -> - expect(@data.memberGroupSubscriptions).to.deep.equal [] - - describe 'when the user has a subscription with recurly', -> - before (done) -> - MockRecurlyApi.accounts['mock-account-id'] = @accounts = { - hosted_login_token: 'mock-login-token' - } - MockRecurlyApi.subscriptions['mock-subscription-id'] = @subscription = { - plan_code: 'collaborator', - tax_in_cents: 100, - tax_rate: 0.2, - unit_amount_in_cents: 500, - currency: 'GBP', - current_period_ends_at: new Date(2018,4,5), - state: 'active', - account_id: 'mock-account-id', - trial_ends_at: new Date(2018, 6, 7) - } - MockRecurlyApi.coupons = @coupons = { - 'test-coupon-1': { description: 'Test Coupon 1' } - 'test-coupon-2': { description: 'Test Coupon 2' } - 'test-coupon-3': { name: 'TestCoupon3' } - } - Subscription.create { - admin_id: @user._id, - manager_ids: [@user._id], - recurlySubscription_id: 'mock-subscription-id', - planCode: 'collaborator' - }, (error) => - return done(error) if error? - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - return - - after (done) -> - MockRecurlyApi.accounts = {} - MockRecurlyApi.subscriptions = {} - MockRecurlyApi.coupons = {} - MockRecurlyApi.redemptions = {} - Subscription.remove { - admin_id: @user._id - }, done - return - - it 'should return a personalSubscription with populated recurly data', -> - subscription = @data.personalSubscription - expect(subscription).to.exist - expect(subscription.planCode).to.equal 'collaborator' - expect(subscription.recurly).to.exist - expect(subscription.recurly).to.deep.equal { - "activeCoupons": [] - "billingDetailsLink": "https://test.recurly.com/account/billing_info/edit?ht=mock-login-token" - "currency": "GBP" - "nextPaymentDueAt": "5th May 2018" - "price": "£6.00" - "state": "active" - "tax": 100 - "taxRate": 0.2 - "trial_ends_at": new Date(2018, 6, 7), - "trialEndsAtFormatted": "7th July 2018" - } - - it 'should return no memberGroupSubscriptions', -> - expect(@data.memberGroupSubscriptions).to.deep.equal [] - - it 'should include redeemed coupons', (done) -> - MockRecurlyApi.redemptions['mock-account-id'] = [ - { state: 'active', coupon_code: 'test-coupon-1' } - { state: 'inactive', coupon_code: 'test-coupon-2' } - { state: 'active', coupon_code: 'test-coupon-3' } - ] - - # rebuild the view model with the redemptions - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, data) -> - expect(error).to.not.exist - expect(data.personalSubscription.recurly.activeCoupons).to.deep.equal [ - { - coupon_code: 'test-coupon-1', - name: '', - description: 'Test Coupon 1' - } - { - coupon_code: 'test-coupon-3', - name: 'TestCoupon3', - description: '' - } - ] - done() - - describe 'when the user has a subscription without recurly', -> - before (done) -> - Subscription.create { - admin_id: @user._id, - manager_ids: [@user._id], - planCode: 'collaborator' - }, (error) => - return done(error) if error? - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - return - - after (done) -> - Subscription.remove { - admin_id: @user._id - }, done - return - - it 'should return a personalSubscription with no recurly data', -> - subscription = @data.personalSubscription - expect(subscription).to.exist - expect(subscription.planCode).to.equal 'collaborator' - expect(subscription.recurly).to.not.exist - - it 'should return no memberGroupSubscriptions', -> - expect(@data.memberGroupSubscriptions).to.deep.equal [] - - describe 'when the user is a member of a group subscription', -> - before (done) -> - @owner1 = new User() - @owner2 = new User() - async.series [ - (cb) => @owner1.ensureUserExists cb - (cb) => @owner2.ensureUserExists cb - (cb) => Subscription.create { - admin_id: @owner1._id, - manager_ids: [@owner1._id], - planCode: 'collaborator', - groupPlan: true, - member_ids: [@user._id] - }, cb - (cb) => Subscription.create { - admin_id: @owner2._id, - manager_ids: [@owner2._id], - planCode: 'collaborator', - groupPlan: true, - member_ids: [@user._id] - }, cb - ], (error) => - return done(error) if error? - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - return - - after (done) -> - Subscription.remove { - admin_id: @owner1._id - }, (error) => - return done(error) if error? - Subscription.remove { - admin_id: @owner2._id - }, done - return - - it 'should return no personalSubscription', -> - expect(@data.personalSubscription).to.equal null - - it 'should return the two memberGroupSubscriptions', -> - expect(@data.memberGroupSubscriptions.length).to.equal 2 - expect( - # Mongoose populates the admin_id with the user - @data.memberGroupSubscriptions[0].admin_id._id.toString() - ).to.equal @owner1._id - expect( - @data.memberGroupSubscriptions[1].admin_id._id.toString() - ).to.equal @owner2._id - - describe 'when the user is a manager of a group subscription', -> - before (done) -> - @owner1 = new User() - @owner2 = new User() - async.series [ - (cb) => @owner1.ensureUserExists cb - (cb) => @owner2.ensureUserExists cb - (cb) => Subscription.create { - admin_id: @owner1._id, - manager_ids: [@owner1._id, @user._id], - planCode: 'collaborator', - groupPlan: true - }, cb - ], (error) => - return done(error) if error? - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - return - - after (done) -> - Subscription.remove { - admin_id: @owner1._id - }, done - return - - it 'should return no personalSubscription', -> - expect(@data.personalSubscription).to.equal null - - it 'should return the managedGroupSubscriptions', -> - expect(@data.managedGroupSubscriptions.length).to.equal 1 - subscription = @data.managedGroupSubscriptions[0] - expect( - # Mongoose populates the admin_id with the user - subscription.admin_id._id.toString() - ).to.equal @owner1._id - expect(subscription.groupPlan).to.equal true - - describe 'when the user is a manager of an institution', -> - before (done) -> - @v1Id = MockV1Api.nextV1Id() - async.series [ - (cb) => - Institution.create({ - v1Id: @v1Id, - managerIds: [@user._id] - }, cb) - ], (error) => - return done(error) if error? - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - return - - after (done) -> - Institution.remove { - v1Id: @v1Id - }, done - return - - it 'should return the managedInstitutions', -> - expect(@data.managedInstitutions.length).to.equal 1 - institution = @data.managedInstitutions[0] - expect(institution.v1Id).to.equal @v1Id - expect(institution.name).to.equal "Institution #{@v1Id}" - - describe 'when the user is a member of an affiliation', -> - before (done) -> - v1Id = MockV1Api.nextV1Id() - MockV1Api.setUser v1Id, { - subscription: {}, - subscription_status: {} - } - MockV1Api.setAffiliations [{ - email: 'confirmed-affiliation-email@stanford.example.edu' - institution: { name: 'Stanford', licence: 'pro_plus', confirmed: true } - }, { - email: 'unconfirmed-affiliation-email@harvard.example.edu' - institution: { name: 'Harvard', licence: 'pro_plus', confirmed: true } - }, { - email: 'confirmed-affiliation-email@mit.example.edu' - institution: { name: 'MIT', licence: 'pro_plus', confirmed: false } - }] - async.series [ - (cb) => - @user.setV1Id v1Id, cb - (cb) => - @user.addEmail 'unconfirmed-affiliation-email@harvard.example.edu', cb - (cb) => - @user.addEmail 'confirmed-affiliation-email@stanford.example.edu', cb - (cb) => - @user.confirmEmail 'confirmed-affiliation-email@stanford.example.edu', cb - (cb) => - @user.addEmail 'confirmed-affiliation-email@mit.example.edu', cb - (cb) => - @user.confirmEmail 'confirmed-affiliation-email@mit.example.edu', cb - ], (error) => - return done(error) if error? - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - - it 'should return only the affilations with confirmed institutions, and confirmed emails', -> - expect(@data.confirmedMemberInstitutions).to.deep.equal [ - { name: 'Stanford', licence: 'pro_plus', confirmed: true } - ] - - describe 'when the user has a v1 subscription', -> - before (done) -> - MockV1Api.setUser v1Id = MockV1Api.nextV1Id(), { - subscription: @subscription = { - trial: false, - has_plan: true, - teams: [{ - id: 56, - name: 'Test team' - }] - } - subscription_status: @subscription_status = { - product: { 'mock': 'product' } - team: null - } - } - @user.setV1Id v1Id, (error) => - return done(error) if error? - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) => - return done(error) if error? - done() - - it 'should return no personalSubscription', -> - expect(@data.personalSubscription).to.equal null - - it 'should return no memberGroupSubscriptions', -> - expect(@data.memberGroupSubscriptions).to.deep.equal [] - - it 'should return a v1SubscriptionStatus', -> - expect(@data.v1SubscriptionStatus).to.deep.equal @subscription_status - - describe 'canceling', -> - before (done) -> - @user = new User() - MockV1Api.setUser v1Id = MockV1Api.nextV1Id(), @v1_user = {} - async.series [ - (cb) => @user.login(cb) - (cb) => @user.setV1Id(v1Id, cb) - ], (error) => - @user.request { - method: 'POST', - url: '/user/subscription/v1/cancel' - }, (error, @response) => - return done(error) if error? - done() - - it 'should tell v1 to cancel the subscription', -> - expect(@v1_user.canceled).to.equal true - - it 'should redirect to the subscription dashboard', -> - expect(@response.statusCode).to.equal 302 - expect(@response.headers.location).to.equal '/user/subscription' diff --git a/services/web/test/acceptance/coffee/TokenAccessTests.coffee b/services/web/test/acceptance/coffee/TokenAccessTests.coffee deleted file mode 100644 index 63b2b4a90a..0000000000 --- a/services/web/test/acceptance/coffee/TokenAccessTests.coffee +++ /dev/null @@ -1,489 +0,0 @@ -expect = require("chai").expect -async = require("async") -MockV1Api = require "./helpers/MockV1Api" -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" -{db, ObjectId} = require("../../../app/js/infrastructure/mongojs") - -try_read_access = (user, project_id, test, callback) -> - async.series [ - (cb) -> - user.request.get "/project/#{project_id}", (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - (cb) -> - user.request.get "/project/#{project_id}/download/zip", (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - ], callback - -try_read_only_token_access = (user, token, test, callback) -> - async.series [ - (cb) -> - user.request.get "/read/#{token}", (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - ], callback - -try_read_and_write_token_access = (user, token, test, callback) -> - async.series [ - (cb) -> - user.request.get "/#{token}", (error, response, body) -> - return cb(error) if error? - test(response, body) - cb() - ], callback - -try_content_access = (user, project_id, test, callback) -> - # The real-time service calls this end point to determine the user's - # permissions. - if user.id? - user_id = user.id - else - user_id = "anonymous-user" - request.post { - url: "/project/#{project_id}/join" - qs: {user_id} - auth: - user: settings.apis.web.user - pass: settings.apis.web.pass - sendImmediately: true - json: true - jar: false - }, (error, response, body) -> - return callback(error) if error? - test(response, body) - callback() - -try_anon_content_access = (user, project_id, token, test, callback) -> - # The real-time service calls this end point to determine the user's - # permissions. - if user.id? - user_id = user.id - else - user_id = "anonymous-user" - request.post { - url: "/project/#{project_id}/join" - qs: {user_id} - auth: - user: settings.apis.web.user - pass: settings.apis.web.pass - sendImmediately: true - headers: - 'x-sl-anonymous-access-token': token - json: true - jar: false - }, (error, response, body) -> - return callback(error) if error? - test(response, body) - callback() - - -describe 'TokenAccess', -> - before (done) -> - @timeout(90000) - @owner = new User() - @other1 = new User() - @other2 = new User() - @anon = new User() - async.parallel [ - (cb) => @owner.login cb - (cb) => @other1.login cb - (cb) => @other2.login cb - (cb) => @anon.getCsrfToken cb - ], done - - describe 'no token-access', -> - before (done) -> - @owner.createProject "token-ro-test#{Math.random()}", (err, project_id) => - return done(err) if err? - @project_id = project_id - # Note, never made token-based, - # thus no tokens - done() - - it 'should deny access ', (done) -> - try_read_access(@other1, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should not allow the user to join the project', (done) -> - try_content_access(@other1, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - - describe 'read-only token', -> - before (done) -> - @owner.createProject "token-ro-test#{Math.random()}", (err, project_id) => - return done(err) if err? - @project_id = project_id - @owner.makeTokenBased @project_id, (err) => - return done(err) if err? - @owner.getProject @project_id, (err, project) => - return done(err) if err? - @tokens = project.tokens - done() - - it 'should deny access before the token is used', (done) -> - try_read_access(@other1, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should allow the user to access project via read-only token url', (done) -> - try_read_only_token_access(@other1, @tokens.readOnly, (response, body) => - expect(response.statusCode).to.equal 200 - , done) - - it 'should allow the user to join the project with read-only access', (done) -> - try_content_access(@other1, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal 'readOnly' - , done) - - describe 'made private again', -> - before (done) -> - @owner.makePrivate @project_id, () -> setTimeout(done, 1000) - - it 'should deny access to project', (done) -> - try_read_access(@other1, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should not allow the user to access read-only token', (done) -> - try_read_only_token_access(@other1, @tokens.readOnly, (response, body) => - expect(response.statusCode).to.equal 404 - , done) - - it 'should not allow the user to join the project', (done) -> - try_content_access(@other1, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - describe 'anonymous read-only token', -> - before (done) -> - @owner.createProject "token-anon-ro-test#{Math.random()}", (err, project_id) => - return done(err) if err? - @project_id = project_id - @owner.makeTokenBased @project_id, (err) => - return done(err) if err? - @owner.getProject @project_id, (err, project) => - return done(err) if err? - @tokens = project.tokens - done() - - it 'should deny access before the token is used', (done) -> - try_read_access(@anon, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should allow the user to access project via read-only token url', (done) -> - try_read_only_token_access(@anon, @tokens.readOnly, (response, body) => - expect(response.statusCode).to.equal 200 - , done) - - it 'should allow the user to anonymously join the project with read-only access', (done) -> - try_anon_content_access(@anon, @project_id, @tokens.readOnly, (response, body) => - expect(body.privilegeLevel).to.equal 'readOnly' - , done) - - describe 'made private again', -> - before (done) -> - @owner.makePrivate @project_id, () -> setTimeout(done, 1000) - - it 'should deny access to project', (done) -> - try_read_access(@anon, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should not allow the user to access read-only token', (done) -> - try_read_only_token_access(@anon, @tokens.readOnly, (response, body) => - expect(response.statusCode).to.equal 404 - , done) - - it 'should not allow the user to join the project', (done) -> - try_anon_content_access(@anon, @project_id, @tokens.readOnly, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - describe 'read-and-write token', -> - before (done) -> - @owner.createProject "token-rw-test#{Math.random()}", (err, project_id) => - return done(err) if err? - @project_id = project_id - @owner.makeTokenBased @project_id, (err) => - return done(err) if err? - @owner.getProject @project_id, (err, project) => - return done(err) if err? - @tokens = project.tokens - done() - - it 'should deny access before the token is used', (done) -> - try_read_access(@other1, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.match /\/restricted.*/ - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should allow the user to access project via read-and-write token url', (done) -> - try_read_and_write_token_access(@other1, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 200 - , done) - - it 'should allow the user to join the project with read-and-write access', (done) -> - try_content_access(@other1, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal 'readAndWrite' - , done) - - describe 'made private again', -> - before (done) -> - @owner.makePrivate @project_id, () -> setTimeout(done, 1000) - - it 'should deny access to project', (done) -> - try_read_access(@other1, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should not allow the user to access read-and-write token', (done) -> - try_read_and_write_token_access(@other1, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 404 - , done) - - it 'should not allow the user to join the project', (done) -> - try_content_access(@other1, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - if !settings.allowAnonymousReadAndWriteSharing - describe 'anonymous read-and-write token, disabled', -> - before (done) -> - @owner.createProject "token-anon-rw-test#{Math.random()}", (err, project_id) => - return done(err) if err? - @project_id = project_id - @owner.makeTokenBased @project_id, (err) => - return done(err) if err? - @owner.getProject @project_id, (err, project) => - return done(err) if err? - @tokens = project.tokens - done() - - it 'should deny access before the token is used', (done) -> - try_read_access(@anon, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should not allow the user to access read-and-write token', (done) -> - try_read_and_write_token_access(@anon, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should not allow the user to join the project', (done) -> - try_anon_content_access(@anon, @project_id, @tokens.readAndWrite, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - else - describe 'anonymous read-and-write token, enabled', -> - before (done) -> - @owner.createProject "token-anon-rw-test#{Math.random()}", (err, project_id) => - return done(err) if err? - @project_id = project_id - @owner.makeTokenBased @project_id, (err) => - return done(err) if err? - @owner.getProject @project_id, (err, project) => - return done(err) if err? - @tokens = project.tokens - done() - - it 'should deny access before the token is used', (done) -> - try_read_access(@anon, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should allow the user to access project via read-and-write token url', (done) -> - try_read_and_write_token_access(@anon, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 200 - , done) - - it 'should allow the user to anonymously join the project with read-and-write access', (done) -> - try_anon_content_access(@anon, @project_id, @tokens.readAndWrite, (response, body) => - expect(body.privilegeLevel).to.equal 'readAndWrite' - , done) - - describe 'made private again', -> - before (done) -> - @owner.makePrivate @project_id, () -> setTimeout(done, 1000) - - it 'should deny access to project', (done) -> - try_read_access(@anon, @project_id, (response, body) => - expect(response.statusCode).to.equal 302 - expect(body).to.match /.*\/restricted.*/ - , done) - - it 'should not allow the user to access read-and-write token', (done) -> - try_read_and_write_token_access(@anon, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 404 - , done) - - it 'should not allow the user to join the project', (done) -> - try_anon_content_access(@anon, @project_id, @tokens.readAndWrite, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - - describe 'private overleaf project', -> - before (done) -> - @owner.createProject 'overleaf-import', (err, project_id) => - @project_id = project_id - @owner.makeTokenBased @project_id, (err) => - @owner.getProject @project_id, (err, project) => - @tokens = project.tokens - @owner.makePrivate @project_id, () => - db.projects.update {_id: project._id}, { - $set: { - overleaf: {id: 1234} - } - }, (err) => - done() - - it 'should redirect to canonical path, when owner uses read-write token', (done) -> - try_read_and_write_token_access(@owner, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/project/#{@project_id}" - , done) - - it 'should allow the owner access to the project', (done) -> - try_read_access(@owner, @project_id, (response, body) => - expect(response.statusCode).to.equal 200 - , done) - - it 'should allow owner to join the project', (done) -> - try_content_access(@owner, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal 'owner' - , done) - - it 'should not allow other user to join the project', (done) -> - try_content_access(@other2, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - describe 'private project, with higher access', -> - before (done) -> - @owner.createProject "higher-access-test-#{Math.random()}", (err, project_id) => - @project_id = project_id - @owner.addUserToProject @project_id, @other1, 'readAndWrite', (err) => - @owner.makeTokenBased @project_id, (err) => - @owner.getProject @project_id, (err, project) => - @tokens = project.tokens - @owner.makePrivate @project_id, () => - setTimeout done, 1000 - - it 'should redirect to canonical path, when user uses read-write token', (done) -> - try_read_and_write_token_access(@other1, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/project/#{@project_id}" - , done) - - it 'should redirect to canonical path, when user uses read-only token', (done) -> - try_read_only_token_access(@other1, @tokens.readOnly, (response, body) => - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/project/#{@project_id}" - , done) - - it 'should allow the user access to the project', (done) -> - try_read_access(@other1, @project_id, (response, body) => - expect(response.statusCode).to.equal 200 - , done) - - it 'should allow user to join the project', (done) -> - try_content_access(@other1, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal 'readAndWrite' - , done) - - it 'should not allow a different user to join the project', (done) -> - try_content_access(@other2, @project_id, (response, body) => - expect(body.privilegeLevel).to.equal false - , done) - - describe 'unimported v1 project', -> - before -> - settings.overleaf = - host: 'http://localhost:5000' - - after -> - delete settings.overleaf - - it 'should redirect read and write token to v1', (done) -> - unimportedV1Token = '123abc' - try_read_and_write_token_access(@owner, unimportedV1Token, (response, body) => - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal( - '/sign_in_to_v1?return_to=/123abc' - ) - , done) - - it 'should redirect read only token to v1', (done) -> - unimportedV1Token = 'abcd' - try_read_only_token_access(@owner, unimportedV1Token, (response, body) => - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal( - '/sign_in_to_v1?return_to=/read/abcd' - ) - , done) - - describe 'importing v1 project', -> - before (done) -> - settings.projectImportingCheckMaxCreateDelta = 3600 - settings.overleaf = - host: 'http://localhost:5000' - @owner.createProject "token-rw-test#{Math.random()}", (err, project_id) => - return done(err) if err? - @project_id = project_id - @owner.makeTokenBased @project_id, (err) => - return done(err) if err? - db.projects.update {_id: ObjectId(project_id)}, $set: overleaf: id: 1234, (err) => - return done(err) if err? - @owner.getProject @project_id, (err, project) => - return done(err) if err? - @tokens = project.tokens - MockV1Api.setDocExported @tokens.readAndWrite, exporting: true - MockV1Api.setDocExported @tokens.readOnly, exporting: true - done() - - after -> - delete settings.projectImportingCheckMaxCreateDelta - delete settings.overleaf - - it 'should show importing page for read and write token', (done) -> - try_read_and_write_token_access(@owner, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 200 - expect(body).to.include('ImportingController') - , done) - - it 'should show importing page for read only token', (done) -> - try_read_only_token_access(@owner, @tokens.readOnly, (response, body) => - expect(response.statusCode).to.equal 200 - expect(body).to.include('ImportingController') - , done) - - describe 'when importing check not configured', -> - before -> - delete settings.projectImportingCheckMaxCreateDelta - - it 'should load editor', (done) -> - try_read_and_write_token_access(@owner, @tokens.readAndWrite, (response, body) => - expect(response.statusCode).to.equal 200 - expect(body).to.include('IdeController') - , done) diff --git a/services/web/test/acceptance/coffee/TpdsUpdateTests.coffee b/services/web/test/acceptance/coffee/TpdsUpdateTests.coffee deleted file mode 100644 index 3f300a55c3..0000000000 --- a/services/web/test/acceptance/coffee/TpdsUpdateTests.coffee +++ /dev/null @@ -1,37 +0,0 @@ -expect = require("chai").expect -ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js" -request = require "./helpers/request" -User = require "./helpers/User" - -describe "TpdsUpdateTests", -> - before (done) -> - @owner = new User() - @owner.login (error) => - throw error if error? - @owner.createProject "test-project", {template: "example"}, (error, project_id) => - throw error if error? - @project_id = project_id - done() - - describe "deleting a file", -> - before (done) -> - request { - method: "DELETE" - url: "/project/#{@project_id}/contents/main.tex" - auth: - username: "sharelatex" - password: "password" - sendImmediately: true - }, (error, response, body) -> - throw error if error? - expect(response.statusCode).to.equal 200 - done() - - it "should have deleted the file", (done) -> - ProjectGetter.getProject @project_id, (error, project) -> - throw error if error? - projectFolder = project.rootFolder[0] - for doc in projectFolder.docs - if doc.name == "main.tex" - throw new Error("expected main.tex to have been deleted") - done() diff --git a/services/web/test/acceptance/coffee/UserEmailsTests.coffee b/services/web/test/acceptance/coffee/UserEmailsTests.coffee deleted file mode 100644 index 3e8ffbd777..0000000000 --- a/services/web/test/acceptance/coffee/UserEmailsTests.coffee +++ /dev/null @@ -1,482 +0,0 @@ -expect = require("chai").expect -async = require("async") -User = require "./helpers/User" -request = require "./helpers/request" -settings = require "settings-sharelatex" -{db, ObjectId} = require("../../../app/js/infrastructure/mongojs") -MockV1Api = require "./helpers/MockV1Api" - -describe "UserEmails", -> - beforeEach (done) -> - @timeout(20000) - @user = new User() - @user.login done - - describe 'confirming an email', -> - it 'should confirm the email', (done) -> - token = null - async.series [ - (cb) => - @user.request { - method: 'POST', - url: '/user/emails', - json: - email: 'newly-added-email@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 204 - cb() - (cb) => - @user.request { url: '/user/emails', json: true }, (error, response, body) -> - expect(response.statusCode).to.equal 200 - expect(body[0].confirmedAt).to.not.exist - expect(body[1].confirmedAt).to.not.exist - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - # There should only be one confirmation token at the moment - expect(tokens.length).to.equal 1 - expect(tokens[0].data.email).to.equal 'newly-added-email@example.com' - expect(tokens[0].data.user_id).to.equal @user._id - token = tokens[0].token - cb() - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/confirm', - json: - token: token - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - cb() - (cb) => - @user.request { url: '/user/emails', json: true }, (error, response, body) -> - expect(response.statusCode).to.equal 200 - expect(body[0].confirmedAt).to.not.exist - expect(body[1].confirmedAt).to.exist - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - # Token should be deleted after use - expect(tokens.length).to.equal 0 - cb() - ], done - - it 'should not allow confirmation of the email if the user has changed', (done) -> - token1 = null - token2 = null - @user2 = new User() - @email = 'duplicate-email@example.com' - async.series [ - (cb) => @user2.login cb - (cb) => - # Create email for first user - @user.request { - method: 'POST', - url: '/user/emails', - json: {@email} - }, cb - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - # There should only be one confirmation token at the moment - expect(tokens.length).to.equal 1 - expect(tokens[0].data.email).to.equal @email - expect(tokens[0].data.user_id).to.equal @user._id - token1 = tokens[0].token - cb() - (cb) => - # Delete the email from the first user - @user.request { - method: 'POST', - url: '/user/emails/delete', - json: {@email} - }, cb - (cb) => - # Create email for second user - @user2.request { - method: 'POST', - url: '/user/emails', - json: {@email} - }, cb - (cb) => - # Original confirmation token should no longer work - @user.request { - method: 'POST', - url: '/user/emails/confirm', - json: - token: token1 - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 404 - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user2._id, - usedAt: { $exists: false } - }, (error, tokens) => - # The first token has been used, so this should be token2 now - expect(tokens.length).to.equal 1 - expect(tokens[0].data.email).to.equal @email - expect(tokens[0].data.user_id).to.equal @user2._id - token2 = tokens[0].token - cb() - (cb) => - # Second user should be able to confirm the email - @user2.request { - method: 'POST', - url: '/user/emails/confirm', - json: - token: token2 - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - cb() - (cb) => - @user2.request { url: '/user/emails', json: true }, (error, response, body) -> - expect(response.statusCode).to.equal 200 - expect(body[0].confirmedAt).to.not.exist - expect(body[1].confirmedAt).to.exist - cb() - ], done - - describe "with an expired token", -> - it 'should not confirm the email', (done) -> - token = null - async.series [ - (cb) => - @user.request { - method: 'POST', - url: '/user/emails', - json: - email: @email = 'expired-token-email@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 204 - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - # There should only be one confirmation token at the moment - expect(tokens.length).to.equal 1 - expect(tokens[0].data.email).to.equal @email - expect(tokens[0].data.user_id).to.equal @user._id - token = tokens[0].token - cb() - (cb) => - db.tokens.update { - token: token - }, { - $set: { - expiresAt: new Date(Date.now() - 1000000) - } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/confirm', - json: - token: token - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 404 - cb() - ], done - - describe 'resending the confirmation', -> - it 'should generate a new token', (done) -> - async.series [ - (cb) => - @user.request { - method: 'POST', - url: '/user/emails', - json: - email: 'reconfirmation-email@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 204 - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - # There should only be one confirmation token at the moment - expect(tokens.length).to.equal 1 - expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' - expect(tokens[0].data.user_id).to.equal @user._id - cb() - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/resend_confirmation', - json: - email: 'reconfirmation-email@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - # There should be two tokens now - expect(tokens.length).to.equal 2 - expect(tokens[0].data.email).to.equal 'reconfirmation-email@example.com' - expect(tokens[0].data.user_id).to.equal @user._id - expect(tokens[1].data.email).to.equal 'reconfirmation-email@example.com' - expect(tokens[1].data.user_id).to.equal @user._id - cb() - ], done - - it 'should create a new token if none exists', (done) -> - # This should only be for users that have sign up with their main - # emails before the confirmation system existed - async.series [ - (cb) => - db.tokens.remove { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/resend_confirmation', - json: - email: @user.email - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - # There should still only be one confirmation token - expect(tokens.length).to.equal 1 - expect(tokens[0].data.email).to.equal @user.email - expect(tokens[0].data.user_id).to.equal @user._id - cb() - ], done - - it "should not allow reconfirmation if the email doesn't match the user", (done) -> - async.series [ - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/resend_confirmation', - json: - email: 'non-matching-email@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 422 - cb() - (cb) => - db.tokens.find { - use: 'email_confirmation', - 'data.user_id': @user._id, - usedAt: { $exists: false } - }, (error, tokens) => - expect(tokens.length).to.equal 0 - cb() - ], done - - describe 'setting a default email', -> - it 'should update confirmed emails for users not in v1', (done) -> - token = null - async.series [ - (cb) => - @user.request { - method: 'POST', - url: '/user/emails', - json: - email: 'new-confirmed-default@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 204 - cb() - (cb) => - # Mark the email as confirmed - db.users.update { - 'emails.email': 'new-confirmed-default@example.com' - }, { - $set: { - 'emails.$.confirmedAt': new Date() - } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/default', - json: - email: 'new-confirmed-default@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - cb() - (cb) => - @user.request { url: '/user/emails', json: true }, (error, response, body) -> - expect(response.statusCode).to.equal 200 - expect(body[0].confirmedAt).to.not.exist - expect(body[0].default).to.equal false - expect(body[1].confirmedAt).to.exist - expect(body[1].default).to.equal true - cb() - ], done - - it 'should not allow changing unconfirmed emails in v1', (done) -> - token = null - async.series [ - (cb) => - db.users.update { - _id: ObjectId(@user._id) - }, { - $set: { - 'overleaf.id': 42 - } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails', - json: - email: 'new-unconfirmed-default@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 204 - cb() - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/default', - json: - email: 'new-unconfirmed-default@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 409 - cb() - (cb) => - @user.request { url: '/user/emails', json: true }, (error, response, body) -> - expect(body[0].default).to.equal true - expect(body[1].default).to.equal false - cb() - ], done - - it 'should update the email in v1 if confirmed', (done) -> - token = null - async.series [ - (cb) => - db.users.update { - _id: ObjectId(@user._id) - }, { - $set: { - 'overleaf.id': 42 - } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails', - json: - email: 'new-confirmed-default-in-v1@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 204 - cb() - (cb) => - # Mark the email as confirmed - db.users.update { - 'emails.email': 'new-confirmed-default-in-v1@example.com' - }, { - $set: { - 'emails.$.confirmedAt': new Date() - } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/default', - json: - email: 'new-confirmed-default-in-v1@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 200 - cb() - ], (error) => - return done(error) if error? - expect( - MockV1Api.updateEmail.calledWith(42, 'new-confirmed-default-in-v1@example.com') - ).to.equal true - done() - - it 'should return an error if the email exists in v1', (done) -> - MockV1Api.existingEmails.push 'exists-in-v1@example.com' - async.series [ - (cb) => - db.users.update { - _id: ObjectId(@user._id) - }, { - $set: { - 'overleaf.id': 42 - } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails', - json: - email: 'exists-in-v1@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 204 - cb() - (cb) => - # Mark the email as confirmed - db.users.update { - 'emails.email': 'exists-in-v1@example.com' - }, { - $set: { - 'emails.$.confirmedAt': new Date() - } - }, cb - (cb) => - @user.request { - method: 'POST', - url: '/user/emails/default', - json: - email: 'exists-in-v1@example.com' - }, (error, response, body) => - return done(error) if error? - expect(response.statusCode).to.equal 409 - expect(body).to.deep.equal { - message: "This email is already registered" - } - cb() - (cb) => - @user.request { url: '/user/emails', json: true }, (error, response, body) -> - expect(body[0].default).to.equal true - expect(body[1].default).to.equal false - cb() - ], done \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/UserReconfirmTests.coffee b/services/web/test/acceptance/coffee/UserReconfirmTests.coffee deleted file mode 100644 index 7a7e5b17c1..0000000000 --- a/services/web/test/acceptance/coffee/UserReconfirmTests.coffee +++ /dev/null @@ -1,33 +0,0 @@ -expect = require("chai").expect -should = require('chai').should() -async = require("async") -User = require "./helpers/User" - -describe 'User Must Reconfirm', -> - - before (done) -> - @user = new User() - async.series [ - @user.ensureUserExists.bind(@user) - (cb) => @user.mongoUpdate {$set: {'must_reconfirm': true}}, cb - ], done - - it 'should not allow sign in', (done) -> - @user.login (err) => - expect(err?).to.equal false - @user.isLoggedIn (err, isLoggedIn) -> - expect(isLoggedIn).to.equal false - done() - - describe 'Requesting reconfirmation email', -> - it 'should return a success to client for existing account', (done) -> - @user.reconfirmAccountRequest @user.email, (err, response) => - expect(err?).to.equal false - expect(response.statusCode).to.equal 200 - done() - - it 'should return a 404 to client for non-existent account', (done) -> - @user.reconfirmAccountRequest 'fake@overleaf.com', (err, response) => - expect(err?).to.equal false - expect(response.statusCode).to.equal 404 - done() \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/UserThirdPartyIdentityTests.coffee b/services/web/test/acceptance/coffee/UserThirdPartyIdentityTests.coffee deleted file mode 100644 index 0529e6b6d5..0000000000 --- a/services/web/test/acceptance/coffee/UserThirdPartyIdentityTests.coffee +++ /dev/null @@ -1,94 +0,0 @@ -Errors = require "../../../app/js/Features/Errors/Errors" -Settings = require "settings-sharelatex" -User = require "./helpers/User" -ThirdPartyIdentityManager = require "../../../app/js/Features/User/ThirdPartyIdentityManager" -chai = require "chai" - -expect = chai.expect - -describe "ThirdPartyIdentityManager", -> - beforeEach (done) -> - @provider = "provider" - @externalUserId = "external-user-id" - @externalData = test: "data" - @user = new User() - @user.ensureUserExists done - - afterEach (done) -> - @user.full_delete_user @user.email, done - - describe "login", -> - describe "when third party identity exists", -> - beforeEach (done) -> - ThirdPartyIdentityManager.link @user.id, @provider, @externalUserId, @externalData, done - - it "should return user", (done) -> - ThirdPartyIdentityManager.login @provider, @externalUserId, @externalData, (err, user) => - expect(err).to.be.null - expect(user._id.toString()).to.equal @user.id - done() - return - - it "should merge external data", (done) -> - @externalData = - test: "different" - another: "key" - ThirdPartyIdentityManager.login @provider, @externalUserId, @externalData, (err, user) => - expect(err).to.be.null - expect(user.thirdPartyIdentifiers[0].externalData).to.deep.equal @externalData - done() - return - - describe "when third party identity does not exists", -> - it "should return error", (done) -> - ThirdPartyIdentityManager.login @provider, @externalUserId, @externalData, (err, user) => - expect(err.name).to.equal "ThirdPartyUserNotFoundError" - done() - return - - describe "link", -> - describe "when provider not already linked", -> - it "should link provider to user", (done) -> - ThirdPartyIdentityManager.link @user.id, @provider, @externalUserId, @externalData, (err, res) -> - expect(res.nModified).to.equal 1 - done() - - describe "when provider is already linked", -> - beforeEach (done) -> - ThirdPartyIdentityManager.link @user.id, @provider, @externalUserId, @externalData, done - - it "should link provider to user", (done) -> - ThirdPartyIdentityManager.link @user.id, @provider, @externalUserId, @externalData, (err, res) -> - expect(res.nModified).to.equal 1 - done() - - it "should not create duplicate thirdPartyIdentifiers", (done) -> - ThirdPartyIdentityManager.link @user.id, @provider, @externalUserId, @externalData, (err, res) => - @user.get (err, user) -> - expect(user.thirdPartyIdentifiers.length).to.equal 1 - done() - - it "should replace existing data", (done) -> - @externalData = replace: "data" - ThirdPartyIdentityManager.link @user.id, @provider, @externalUserId, @externalData, (err, res) => - @user.get (err, user) => - expect(user.thirdPartyIdentifiers[0].externalData).to.deep.equal @externalData - done() - - describe "unlink", -> - describe "when provider not already linked", -> - it "should succeed", (done) -> - ThirdPartyIdentityManager.unlink @user.id, @provider, (err, res) -> - expect(err).to.be.null - expect(res.nModified).to.equal 0 - done() - - describe "when provider is already linked", -> - beforeEach (done) -> - ThirdPartyIdentityManager.link @user.id, @provider, @externalUserId, @externalData, done - - it "should remove thirdPartyIdentifiers entry", (done) -> - ThirdPartyIdentityManager.unlink @user.id, @provider, (err, res) => - @user.get (err, user) -> - expect(user.thirdPartyIdentifiers.length).to.equal 0 - done() diff --git a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee deleted file mode 100644 index 0711ac613f..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee +++ /dev/null @@ -1,50 +0,0 @@ -express = require("express") -bodyParser = require "body-parser" -app = express() - -module.exports = MockClsiApi = - run: () -> - - compile = (req, res, next) => - res.status(200).send { - compile: - status: 'success' - error: null - outputFiles: [ - url: "/project/#{req.params.project_id}/build/1234/output/project.pdf" - path: 'project.pdf' - type: 'pdf' - build: 1234 - , - url: "/project/#{req.params.project_id}/build/1234/output/project.log" - path: 'project.log' - type: 'log' - build: 1234 - ] - } - - app.post "/project/:project_id/compile", compile - app.post "/project/:project_id/user/:user_id/compile", compile - - app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) -> - filename = req.params[0] - if filename == 'project.pdf' - res.status(200).send 'mock-pdf' - else if filename == 'project.log' - res.status(200).send 'mock-log' - else - res.sendStatus(404) - - app.get "/project/:project_id/user/:user_id/build/:build_id/output/:output_path", (req, res, next) => - res.status(200).send("hello") - - app.get "/project/:project_id/status", (req, res, next) => - res.status(200).send() - - app.listen 3013, (error) -> - throw error if error? - .on "error", (error) -> - console.error "error starting MockClsiApi:", error.message - process.exit(1) - -MockClsiApi.run() diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee deleted file mode 100644 index a0058dff62..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee +++ /dev/null @@ -1,59 +0,0 @@ -express = require("express") -app = express() -bodyParser = require "body-parser" -jsonParser = bodyParser.json() - -module.exports = MockDocUpdaterApi = - updates: {} - - clearProjectStructureUpdates: () -> - @updates = {} - - getProjectStructureUpdates: (project_id) -> - @updates[project_id] || { docUpdates: [], fileUpdates: [] } - - addProjectStructureUpdates: (project_id, userId, docUpdates, fileUpdates, version) -> - @updates[project_id] ||= { docUpdates: [], fileUpdates: [] } - - for update in docUpdates - update.userId = userId - @updates[project_id].docUpdates.push(update) - - for update in fileUpdates - update.userId = userId - @updates[project_id].fileUpdates.push(update) - - @updates[project_id].version = version - - run: () -> - app.post "/project/:project_id/flush", (req, res, next) => - res.sendStatus 204 - - app.post "/project/:project_id", jsonParser, (req, res, next) => - project_id = req.params.project_id - {userId, docUpdates, fileUpdates, version} = req.body - @addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates, version) - res.sendStatus 200 - - app.post "/project/:project_id/doc/:doc_id", (req, res, next) => - res.sendStatus 204 - - app.delete "/project/:project_id", (req, res) => - res.sendStatus 204 - - app.post "/project/:project_id/doc/:doc_id/flush", (req, res, next) => - res.sendStatus 204 - - app.delete "/project/:project_id/doc/:doc_id", (req, res, next) => - res.sendStatus 204 - - app.post "/project/:project_id/history/resync", (req, res, next) => - res.sendStatus 204 - - app.listen 3003, (error) -> - throw error if error? - .on "error", (error) -> - console.error "error starting MockDocUpdaterApi:", error.message - process.exit(1) - -MockDocUpdaterApi.run() diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee deleted file mode 100644 index 96fc75dc59..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee +++ /dev/null @@ -1,52 +0,0 @@ -express = require("express") -bodyParser = require "body-parser" -app = express() - -module.exports = MockDocStoreApi = - docs: {} - - run: () -> - app.post "/project/:project_id/doc/:doc_id", bodyParser.json(), (req, res, next) => - {project_id, doc_id} = req.params - {lines, version, ranges} = req.body - @docs[project_id] ?= {} - @docs[project_id][doc_id] = {lines, version, ranges} - @docs[project_id][doc_id].rev ?= 0 - @docs[project_id][doc_id].rev += 1 - @docs[project_id][doc_id]._id = doc_id - res.json { - modified: true - rev: @docs[project_id][doc_id].rev - } - - app.get "/project/:project_id/doc", (req, res, next) => - docs = (doc for doc_id, doc of @docs[req.params.project_id]) - res.json docs - - app.get "/project/:project_id/doc/:doc_id", (req, res, next) => - {project_id, doc_id} = req.params - doc = @docs[project_id][doc_id] - if !doc? or (doc.deleted and !req.query.include_deleted) - res.sendStatus 404 - else - res.json doc - - app.delete "/project/:project_id/doc/:doc_id", (req, res, next) => - {project_id, doc_id} = req.params - if !@docs[project_id]? - res.sendStatus 404 - else if !@docs[project_id][doc_id]? - res.sendStatus 404 - else - @docs[project_id][doc_id].deleted = true - res.sendStatus 204 - - app.listen 3016, (error) -> - throw error if error? - .on "error", (error) -> - console.error "error starting MockDocStoreApi:", error.message - process.exit(1) - - -MockDocStoreApi.run() - diff --git a/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee deleted file mode 100644 index 5a7d70efa9..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockFileStoreApi.coffee +++ /dev/null @@ -1,44 +0,0 @@ -express = require("express") -bodyParser = require "body-parser" -app = express() - -module.exports = MockFileStoreApi = - files: {} - - run: () -> - app.post "/project/:project_id/file/:file_id", (req, res, next) => - chunks = [] - req.on 'data', (chunk) -> - chunks.push(chunk) - - req.on 'end', => - content = Buffer.concat(chunks).toString() - {project_id, file_id} = req.params - @files[project_id] ?= {} - @files[project_id][file_id] = { content } - res.sendStatus 200 - - app.get "/project/:project_id/file/:file_id", (req, res, next) => - {project_id, file_id} = req.params - { content } = @files[project_id][file_id] - res.send content - - # handle file copying - app.put "/project/:project_id/file/:file_id", bodyParser.json(), (req, res, next) => - {project_id, file_id} = req.params - source = req.body.source - {content} = @files[source.project_id]?[source.file_id] - if !content? - res.sendStatus 500 - else - @files[project_id] ?= {} - @files[project_id][file_id] = { content } - res.sendStatus 200 - - app.listen 3009, (error) -> - throw error if error? - .on "error", (error) -> - console.error "error starting MockFileStoreApi:", error.message - process.exit(1) - -MockFileStoreApi.run() diff --git a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee deleted file mode 100644 index 26a58e7a5a..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee +++ /dev/null @@ -1,108 +0,0 @@ -_ = require 'lodash' -express = require 'express' -bodyParser = require "body-parser" -app = express() -{ObjectId} = require 'mongojs' - -module.exports = MockProjectHistoryApi = - docs: {} - - oldFiles: {} - - projectVersions: {} - - labels: {} - - projectSnapshots: {} - - addOldFile: (project_id, version, pathname, content) -> - @oldFiles["#{project_id}:#{version}:#{pathname}"] = content - - addProjectSnapshot: (project_id, version, snapshot) -> - @projectSnapshots["#{project_id}:#{version}"] = snapshot - - setProjectVersion: (project_id, version) -> - @projectVersions[project_id] = {version: version} - - setProjectVersionInfo: (project_id, versionInfo) -> - @projectVersions[project_id] = versionInfo - - addLabel: (project_id, label) -> - if !label.id? - label.id = new ObjectId().toString() - @labels[project_id] ?= {} - @labels[project_id][label.id] = label - - deleteLabel: (project_id, label_id) -> - delete @labels[project_id][label_id] - - getLabels: (project_id) -> - return null unless @labels[project_id]? - _.values @labels[project_id] - - reset: () -> - @oldFiles = {} - @projectVersions = {} - @labels = {} - - run: () -> - app.post "/project", (req, res, next) => - res.json project: id: 1 - - app.get "/project/:project_id/version/:version/:pathname", (req, res, next) => - {project_id, version, pathname} = req.params - key = "#{project_id}:#{version}:#{pathname}" - if @oldFiles[key]? - res.send @oldFiles[key] - else - res.send 404 - - app.get "/project/:project_id/version/:version", (req, res, next) => - {project_id, version} = req.params - key = "#{project_id}:#{version}" - if @projectSnapshots[key]? - res.json @projectSnapshots[key] - else - res.sendStatus 404 - - app.get "/project/:project_id/version", (req, res, next) => - {project_id} = req.params - if @projectVersions[project_id]? - res.json @projectVersions[project_id] - else - res.send 404 - - app.get "/project/:project_id/labels", (req, res, next) => - {project_id} = req.params - labels = @getLabels project_id - if labels? - res.json labels - else - res.send 404 - - app.post "/project/:project_id/user/:user_id/labels", bodyParser.json(), (req, res, next) => - {project_id} = req.params - {comment, version} = req.body - label_id = new ObjectId().toString() - @addLabel project_id, {id: label_id, comment, version} - res.json {label_id, comment, version} - - app.delete "/project/:project_id/user/:user_id/labels/:label_id", (req, res, next) => - {project_id, label_id} = req.params - label = @labels[project_id]?[label_id] - if label? - @deleteLabel project_id, label_id - res.send 204 - else - res.send 404 - - app.post "/project/:project_id/flush", (req, res, next) => - res.sendStatus 200 - - app.listen 3054, (error) -> - throw error if error? - .on "error", (error) -> - console.error "error starting MockProjectHistoryApi:", error.message - process.exit(1) - -MockProjectHistoryApi.run() diff --git a/services/web/test/acceptance/coffee/helpers/MockRecurlyApi.coffee b/services/web/test/acceptance/coffee/helpers/MockRecurlyApi.coffee deleted file mode 100644 index 23692c6ac4..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockRecurlyApi.coffee +++ /dev/null @@ -1,81 +0,0 @@ -express = require("express") -app = express() -bodyParser = require('body-parser') - -app.use(bodyParser.json()) - -module.exports = MockRecurlyApi = - subscriptions: {} - - accounts: {} - - redemptions: {} - - coupons: {} - - run: () -> - app.get '/subscriptions/:id', (req, res, next) => - subscription = @subscriptions[req.params.id] - if !subscription? - res.status(404).end() - else - res.send """ - - #{subscription.plan_code} - #{subscription.currency} - #{subscription.state} - #{subscription.tax_in_cents} - #{subscription.tax_rate} - #{subscription.current_period_ends_at} - #{subscription.unit_amount_in_cents} - - #{subscription.trial_ends_at} - - """ - - app.get '/accounts/:id', (req, res, next) => - account = @accounts[req.params.id] - if !account? - res.status(404).end() - else - res.send """ - - #{req.params.id} - #{account.hosted_login_token} - - """ - - app.get '/coupons/:code', (req, res, next) => - coupon = @coupons[req.params.code] - if !coupon? - res.status(404).end() - else - res.send """ - - #{req.params.code} - #{coupon.name or ''} - #{coupon.description or ''} - - """ - - app.get '/accounts/:id/redemptions', (req, res, next) => - redemptions = @redemptions[req.params.id] or [] - redemptionsListXml = '' - for redemption in redemptions - redemptionsListXml += """ - - #{redemption.state} - #{redemption.coupon_code} - - """ - - res.send """ - - #{redemptionsListXml} - - """ - - app.listen 6034, (error) -> - throw error if error? - -MockRecurlyApi.run() diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee deleted file mode 100644 index 5ea163aeec..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ /dev/null @@ -1,174 +0,0 @@ -express = require("express") -app = express() -bodyParser = require('body-parser') -sinon = require 'sinon' - -app.use(bodyParser.json()) - -v1Id = 1000 - -module.exports = MockV1Api = - nextV1Id: -> v1Id++ - - users: { } - - setUser: (id, user) -> - @users[id] = user - - exportId: null - - exportParams: null - - setExportId: (id) -> - @exportId = id - - getLastExportParams: () -> - @exportParams - - clearExportParams: () -> - @exportParams = null - - syncUserFeatures: sinon.stub() - - affiliations: [] - - updateEmail: sinon.stub() - - existingEmails: [] - - brands: {} - - brand_variations: {} - - validation_clients: {} - - setAffiliations: (affiliations) -> @affiliations = affiliations - - doc_exported: {} - - setDocExported: (token, info) -> @doc_exported[token] = info - - run: () -> - app.get "/api/v1/sharelatex/users/:v1_user_id/plan_code", (req, res, next) => - user = @users[req.params.v1_user_id] - if user - res.json user - else - res.sendStatus 404 - - app.get "/api/v1/sharelatex/users/:v1_user_id/subscriptions", (req, res, next) => - user = @users[req.params.v1_user_id] - if user?.subscription? - res.json user.subscription - else - res.sendStatus 404 - - app.get "/api/v1/sharelatex/users/:v1_user_id/subscription_status", (req, res, next) => - user = @users[req.params.v1_user_id] - if user?.subscription_status? - res.json user.subscription_status - else - res.sendStatus 404 - - app.delete "/api/v1/sharelatex/users/:v1_user_id/subscription", (req, res, next) => - user = @users[req.params.v1_user_id] - if user? - user.canceled = true - res.sendStatus 200 - else - res.sendStatus 404 - - app.post "/api/v1/sharelatex/users/:v1_user_id/sync", (req, res, next) => - @syncUserFeatures(req.params.v1_user_id) - res.sendStatus 200 - - app.post "/api/v1/sharelatex/exports", (req, res, next) => - @exportParams = Object.assign({}, req.body) - res.json exportId: @exportId - - app.get "/api/v2/users/:userId/affiliations", (req, res, next) => - res.json @affiliations - - app.post "/api/v2/users/:userId/affiliations", (req, res, next) => - res.sendStatus 201 - - app.delete "/api/v2/users/:userId/affiliations/:email", (req, res, next) => - res.sendStatus 204 - - app.get "/api/v2/brands/:slug", (req, res, next) => - if brand = @brands[req.params.slug] - res.json brand - else - res.sendStatus 404 - - app.get '/universities/list', (req, res, next) -> - res.json [] - - app.get '/universities/list/:id', (req, res, next) -> - res.json { - id: parseInt(req.params.id) - name: "Institution #{req.params.id}" - } - - app.get '/university/domains', (req, res, next) -> - res.json [] - - app.put '/api/v1/sharelatex/users/:id/email', (req, res, next) => - { email } = req.body?.user - if email in @existingEmails - return res.sendStatus 409 - else - @updateEmail parseInt(req.params.id), email - return res.sendStatus 200 - - app.post "/api/v1/sharelatex/login", (req, res, next) => - for id, user of @users - if user? && user.email == req.body.email && user.password == req.body.password - return res.json { - email: user.email, - valid: true, - user_profile: user.profile - } - return res.status(403).json { - email: req.body.email, - valid: false - } - - app.get "/api/v2/partners/:partner/conversions/:id", (req, res, next) => - partner = @validation_clients[req.params.partner] - conversion = partner?.conversions?[req.params.id] - if conversion? - res.status(200).json {input_file_uri: conversion, brand_variation_id: partner.brand_variation_id} - else - res.status(404).json {} - - app.get "/api/v2/brand_variations/:id", (req, res, next) => - variation = @brand_variations[req.params.id] - if variation? - res.status(200).json variation - else - res.status(404).json {} - - app.get '/api/v1/sharelatex/docs/:token/is_published', (req, res, next) => - res.json { allow: true } - - app.get '/api/v1/sharelatex/users/:user_id/docs/:token/info', (req, res, next) => - res.json { - exists: true, - exported: false - } - - app.get '/api/v1/sharelatex/docs/:token/exported_to_v2', (req, res, next) => - return res.json @doc_exported[req.params.token] if @doc_exported[req.params.token]? - res.json { exporting: false, exported: false } - - app.get '/api/v1/sharelatex/docs/read_token/:token/exists', (req, res, next) => - res.json { exists: false } - - app.listen 5000, (error) -> - throw error if error? - .on "error", (error) -> - console.error "error starting MockV1Api:", error.message - process.exit(1) - -MockV1Api.run() diff --git a/services/web/test/acceptance/coffee/helpers/MockV1HistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockV1HistoryApi.coffee deleted file mode 100644 index c0adee8edd..0000000000 --- a/services/web/test/acceptance/coffee/helpers/MockV1HistoryApi.coffee +++ /dev/null @@ -1,30 +0,0 @@ -_ = require 'lodash' -express = require 'express' -bodyParser = require "body-parser" -app = express() -{ObjectId} = require 'mongojs' - -module.exports = MockV1HistoryApi = - fakeZipCall: 0 - run: () -> - app.get "/api/projects/:project_id/version/:version/zip", (req, res, next) => - res.header('content-disposition', 'attachment; name=project.zip') - res.header('content-type', 'application/octet-stream') - res.send "Mock zip for #{req.params.project_id} at version #{req.params.version}" - - app.get "/fake-zip-download/:project_id/version/:version", (req, res, next) => - return res.sendStatus 404 unless @fakeZipCall++ > 0 - res.header('content-disposition', 'attachment; name=project.zip') - res.header('content-type', 'application/octet-stream') - res.send "Mock zip for #{req.params.project_id} at version #{req.params.version}" - - app.post "/api/projects/:project_id/version/:version/zip", (req, res, next) => - res.json zipUrl: "http://localhost:3100/fake-zip-download/#{req.params.project_id}/version/#{req.params.version}" - - app.listen 3100, (error) -> - throw error if error? - .on "error", (error) -> - console.error "error starting MockV1HistoryApi:", error.message - process.exit(1) - -MockV1HistoryApi.run() diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee deleted file mode 100644 index 314bdc6942..0000000000 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ /dev/null @@ -1,339 +0,0 @@ -request = require("./request") -_ = require("underscore") -settings = require("settings-sharelatex") -{db, ObjectId} = require("../../../../app/js/infrastructure/mongojs") -UserModel = require("../../../../app/js/models/User").User -UserUpdater = require("../../../../app/js/Features/User/UserUpdater") -AuthenticationManager = require("../../../../app/js/Features/Authentication/AuthenticationManager") - -count = 0 - -class User - constructor: (options = {}) -> - @emails = [ - email: options.email || "acceptance-test-#{count}@example.com" - createdAt: new Date() - ] - @email = @emails[0].email - @password = "acceptance-test-#{count}-password" - count++ - @jar = request.jar() - @request = request.defaults({ - jar: @jar - }) - - setExtraAttributes: (user) -> - throw new Error("User does not exist") unless user?._id? - @id = user._id.toString() - @_id = user._id.toString() - @first_name = user.first_name - @referal_id = user.referal_id - - get: (callback = (error, user)->) -> - db.users.findOne { _id: ObjectId(@_id) }, callback - - mongoUpdate: (updateOp, callback=(error)->) -> - db.users.update {_id: ObjectId(@_id)}, updateOp, callback - - register: (callback = (error, user) ->) -> - @registerWithQuery('', callback) - - registerWithQuery: (query, callback = (error, user) ->) -> - return callback(new Error('User already registered')) if @_id? - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: '/register' + query - json: { @email, @password } - }, (error, response, body) => - return callback(error) if error? - db.users.findOne { email: @email }, (error, user) => - return callback(error) if error? - @setExtraAttributes user - callback(null, user) - - login: (callback = (error) ->) -> - @loginWith(@email, callback) - - loginWith: (email, callback = (error) ->) -> - @ensureUserExists (error) => - return callback(error) if error? - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: if settings.enableLegacyLogin then "/login/legacy" else "/login" - json: { email, @password } - }, callback - - ensureUserExists: (callback = (error) ->) -> - filter = {@email} - options = {upsert: true, new: true, setDefaultsOnInsert: true} - UserModel.findOneAndUpdate filter, {}, options, (error, user) => - return callback(error) if error? - AuthenticationManager.setUserPasswordInV2 user._id, @password, (error) => - return callback(error) if error? - UserUpdater.updateUser user._id, $set: emails: @emails, (error) => - return callback(error) if error? - @setExtraAttributes user - callback(null, @password) - - setFeatures: (features, callback = (error) ->) -> - update = {} - for key, value of features - update["features.#{key}"] = value - UserModel.update { _id: @id }, update, callback - - setOverleafId: (overleaf_id, callback = (error) ->) -> - UserModel.update { _id: @id }, { 'overleaf.id': overleaf_id }, callback - - logout: (callback = (error) ->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: "/logout" - json: - email: @email - password: @password - }, (error, response, body) => - return callback(error) if error? - db.users.findOne {email: @email}, (error, user) => - return callback(error) if error? - @id = user?._id?.toString() - @_id = user?._id?.toString() - callback() - - addEmail: (email, callback = (error) ->) -> - @emails.push(email: email, createdAt: new Date()) - UserUpdater.addEmailAddress @id, email, callback - - confirmEmail: (email, callback = (error) ->) -> - for emailData, idx in @emails - @emails[idx].confirmedAt = new Date() if emailData.email == email - UserUpdater.confirmEmail @id, email, callback - - ensure_admin: (callback = (error) ->) -> - db.users.update {_id: ObjectId(@id)}, { $set: { isAdmin: true }}, callback - - upgradeFeatures: (callback = (error) -> ) -> - features = { - collaborators: -1 # Infinite - versioning: true - dropbox:true - compileTimeout: 60 - compileGroup:"priority" - templates: true - references: true - trackChanges: true - trackChangesVisible: true - } - db.users.update {_id: ObjectId(@id)}, { $set: { features: features }}, callback - - downgradeFeatures: (callback = (error) -> ) -> - features = { - collaborators: 1 - versioning: false - dropbox:false - compileTimeout: 60 - compileGroup:"standard" - templates: false - references: false - trackChanges: false - trackChangesVisible: false - } - db.users.update {_id: ObjectId(@id)}, { $set: { features: features }}, callback - - defaultFeatures: (callback = (error) -> ) -> - features = settings.defaultFeatures - db.users.update {_id: ObjectId(@id)}, { $set: { features: features }}, callback - - full_delete_user: (email, callback = (error) ->) -> - db.users.findOne {email: email}, (error, user) => - if !user? - return callback() - user_id = user._id - db.projects.remove owner_ref:ObjectId(user_id), {multi:true}, (err)-> - if err? - callback(err) - db.users.remove {_id: ObjectId(user_id)}, callback - - getProject: (project_id, callback = (error, project)->) -> - db.projects.findOne {_id: ObjectId(project_id.toString())}, callback - - saveProject: (project, callback=(error)->) -> - db.projects.update {_id: project._id}, project, callback - - createProject: (name, options, callback = (error, oroject_id) ->) -> - if typeof options == "function" - callback = options - options = {} - - @request.post { - url: "/project/new", - json: Object.assign({projectName: name}, options) - }, (error, response, body) -> - return callback(error) if error? - if !body?.project_id? - error = new Error("SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body) - callback error - else - callback(null, body.project_id) - - deleteProject: (project_id, callback=(error)) -> - @request.delete { - url: "/project/#{project_id}" - }, (error, response, body) -> - return callback(error) if error? - callback(null) - - deleteProjects: (callback=(error)) -> - db.projects.remove owner_ref:ObjectId(@id), {multi:true}, (err)-> - callback(err) - - openProject: (project_id, callback=(error)) -> - @request.get { - url: "/project/#{project_id}" - }, (error, response, body) -> - return callback(error) if error? - if response.statusCode != 200 - err = new Error("Non-success response when opening project: #{response.statusCode}") - return callback(err) - callback(null) - - createDocInProject: (project_id, parent_folder_id, name, callback=(error, doc_id)->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: "/project/#{project_id}/doc", - json: { - name: name, - parent_folder_id: parent_folder_id - } - }, (error, response, body) => - callback(null, body._id) - - addUserToProject: (project_id, user, privileges, callback = (error, user) ->) -> - if privileges == 'readAndWrite' - updateOp = {$addToSet: {collaberator_refs: user._id.toString()}} - else if privileges == 'readOnly' - updateOp = {$addToSet: {readOnly_refs: user._id.toString()}} - db.projects.update {_id: db.ObjectId(project_id)}, updateOp, (err) -> - callback(err) - - makePublic: (project_id, level, callback = (error) ->) -> - @request.post { - url: "/project/#{project_id}/settings/admin", - json: - publicAccessLevel: level - }, (error, response, body) -> - return callback(error) if error? - callback(null) - - makePrivate: (project_id, callback = (error) ->) -> - @request.post { - url: "/project/#{project_id}/settings/admin", - json: - publicAccessLevel: 'private' - }, (error, response, body) -> - return callback(error) if error? - callback(null) - - makeTokenBased: (project_id, callback = (error) ->) -> - @request.post { - url: "/project/#{project_id}/settings/admin", - json: - publicAccessLevel: 'tokenBased' - }, (error, response, body) -> - return callback(error) if error? - callback(null) - - getCsrfToken: (callback = (error) ->) -> - @request.get { - url: "/dev/csrf" - }, (err, response, body) => - return callback(err) if err? - @csrfToken = body - @request = @request.defaults({ - headers: - "x-csrf-token": @csrfToken - }) - callback() - - changePassword: (callback = (error) ->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: "/user/password/update" - json: - currentPassword: @password - newPassword1: @password - newPassword2: @password - }, (error, response, body) => - return callback(error) if error? - db.users.findOne {email: @email}, (error, user) => - return callback(error) if error? - callback() - - reconfirmAccountRequest: (user_email, callback = (error) ->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: "/user/reconfirm" - json: - email: user_email - }, (error, response, body) => - callback(error, response) - - getUserSettingsPage: (callback = (error, statusCode) ->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.get { - url: "/user/settings" - }, (error, response, body) => - return callback(error) if error? - callback(null, response.statusCode) - - activateSudoMode: (callback = (error)->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - uri: '/confirm-password', - json: - password: @password - }, callback - - updateSettings: (newSettings, callback = (error, response, body) ->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: '/user/settings' - json: newSettings - }, callback - - getProjectListPage: (callback=(error, statusCode)->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.get { - url: "/project" - }, (error, response, body) => - return callback(error) if error? - callback(null, response.statusCode) - - isLoggedIn: (callback = (error, loggedIn) ->) -> - @request.get "/user/personal_info", (error, response, body) -> - return callback(error) if error? - if response.statusCode == 200 - return callback(null, true) - else if response.statusCode == 302 - return callback(null, false) - else - return callback(new Error("unexpected status code from /user/personal_info: #{response.statusCode}")) - - setV1Id: (v1Id, callback) -> - UserModel.update { - _id: @_id - }, { - overleaf: - id: v1Id - }, callback - -module.exports = User diff --git a/services/web/test/acceptance/coffee/helpers/redis.coffee b/services/web/test/acceptance/coffee/helpers/redis.coffee deleted file mode 100644 index 7c48f97d2e..0000000000 --- a/services/web/test/acceptance/coffee/helpers/redis.coffee +++ /dev/null @@ -1,33 +0,0 @@ -Settings = require('settings-sharelatex') -logger = require("logger-sharelatex") -Async = require('async') - -UserSessionsRedis = require('../../../../app/js/Features/User/UserSessionsRedis') - -# rclient = redis.createClient(Settings.redis.web) -rclient = UserSessionsRedis.client() - -module.exports = - - getUserSessions: (user, callback=(err, sessionsSet)->) -> - rclient.smembers UserSessionsRedis.sessionSetKey(user), (err, result) -> - return callback(err, result) - - clearUserSessions: (user, callback=(err)->) -> - sessionSetKey = UserSessionsRedis.sessionSetKey(user) - rclient.smembers sessionSetKey, (err, sessionKeys) -> - if err - return callback(err) - if sessionKeys.length == 0 - return callback(null) - actions = sessionKeys.map (k) -> - (cb) -> - rclient.del k, (err) -> - cb(err) - Async.series( - actions, (err, results) -> - rclient.srem sessionSetKey, sessionKeys, (err) -> - if err - return callback(err) - callback(null) - ) diff --git a/services/web/test/acceptance/coffee/helpers/request.coffee b/services/web/test/acceptance/coffee/helpers/request.coffee deleted file mode 100644 index 1c7120d141..0000000000 --- a/services/web/test/acceptance/coffee/helpers/request.coffee +++ /dev/null @@ -1,5 +0,0 @@ -BASE_URL = "http://#{process.env["HTTP_TEST_HOST"] or "localhost"}:3000" -module.exports = require("request").defaults({ - baseUrl: BASE_URL, - followRedirect: false -}) \ No newline at end of file diff --git a/services/web/test/acceptance/src/ApiClsiTests.js b/services/web/test/acceptance/src/ApiClsiTests.js new file mode 100644 index 0000000000..ef01ab4f83 --- /dev/null +++ b/services/web/test/acceptance/src/ApiClsiTests.js @@ -0,0 +1,149 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const request = require('./helpers/request') +const Settings = require('settings-sharelatex') + +const auth = new Buffer('sharelatex:password').toString('base64') +const authed_request = request.defaults({ + headers: { + Authorization: `Basic ${auth}` + } +}) + +describe('ApiClsiTests', function() { + describe('compile', function() { + before(function(done) { + this.compileSpec = { + compile: { + options: { + compiler: 'pdflatex', + timeout: 60 + }, + rootResourcePath: 'main.tex', + resources: [ + { + path: 'main/tex', + content: + '\\documentclass{article}\n\\begin{document}\nHello World\n\\end{document}' + }, + { + path: 'image.png', + url: 'www.example.com/image.png', + modified: 123456789 + } + ] + } + } + return done() + }) + + describe('valid request', () => + it('returns success and a list of output files', function(done) { + return authed_request.post( + { + uri: '/api/clsi/compile/abcd', + json: this.compileSpec + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + expect(response.body).to.deep.equal({ + status: 'success', + outputFiles: [ + { + path: 'project.pdf', + url: '/project/abcd/build/1234/output/project.pdf', + type: 'pdf', + build: 1234 + }, + { + path: 'project.log', + url: '/project/abcd/build/1234/output/project.log', + type: 'log', + build: 1234 + } + ] + }) + return done() + } + ) + })) + + return describe('unauthorized', () => + it('returns 401', function(done) { + return request.post( + { + uri: '/api/clsi/compile/abcd', + json: this.compileSpec + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(401) + expect(response.body).to.equal('Unauthorized') + return done() + } + ) + })) + }) + + return describe('get output', function() { + describe('valid file', () => + it('returns the file', done => + authed_request.get( + '/api/clsi/compile/abcd/build/1234/output/project.pdf', + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + expect(response.body).to.equal('mock-pdf') + return done() + } + ))) + + describe('invalid file', () => + it('returns 404', done => + authed_request.get( + '/api/clsi/compile/abcd/build/1234/output/project.aux', + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(404) + expect(response.body).to.not.equal('mock-pdf') + return done() + } + ))) + + return describe('unauthorized', () => + it('returns 401', done => + request.get( + '/api/clsi/compile/abcd/build/1234/output/project.pdf', + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(401) + expect(response.body).to.not.equal('mock-pdf') + return done() + } + ))) + }) +}) diff --git a/services/web/test/acceptance/src/AuthorizationTests.js b/services/web/test/acceptance/src/AuthorizationTests.js new file mode 100644 index 0000000000..2f3836e24f --- /dev/null +++ b/services/web/test/acceptance/src/AuthorizationTests.js @@ -0,0 +1,618 @@ +/* eslint-disable + camelcase, + max-len, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const User = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') + +const MockDocstoreApi = require('./helpers/MockDocstoreApi') +const MockDocUpdaterApi = require('./helpers/MockDocUpdaterApi') + +const try_read_access = (user, project_id, test, callback) => + async.series( + [ + cb => + user.request.get(`/project/${project_id}`, function( + error, + response, + body + ) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + }), + cb => + user.request.get(`/project/${project_id}/download/zip`, function( + error, + response, + body + ) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + }) + ], + callback + ) + +const try_settings_write_access = (user, project_id, test, callback) => + async.series( + [ + cb => + user.request.post( + { + uri: `/project/${project_id}/settings`, + json: { + compiler: 'latex' + } + }, + function(error, response, body) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + } + ) + ], + callback + ) + +const try_admin_access = (user, project_id, test, callback) => + async.series( + [ + cb => + user.request.post( + { + uri: `/project/${project_id}/rename`, + json: { + newProjectName: 'new-name' + } + }, + function(error, response, body) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + } + ), + cb => + user.request.post( + { + uri: `/project/${project_id}/settings/admin`, + json: { + publicAccessLevel: 'private' + } + }, + function(error, response, body) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + } + ) + ], + callback + ) + +const try_content_access = function(user, project_id, test, callback) { + // The real-time service calls this end point to determine the user's + // permissions. + let user_id + if (user.id != null) { + user_id = user.id + } else { + user_id = 'anonymous-user' + } + return request.post( + { + url: `/project/${project_id}/join`, + qs: { user_id }, + auth: { + user: settings.apis.web.user, + pass: settings.apis.web.pass, + sendImmediately: true + }, + json: true, + jar: false + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + test(response, body) + return callback() + } + ) +} + +const expect_read_access = (user, project_id, callback) => + async.series( + [ + cb => + try_read_access( + user, + project_id, + (response, body) => + expect(response.statusCode).to.be.oneOf([200, 204]), + cb + ), + cb => + try_content_access( + user, + project_id, + (response, body) => + expect(body.privilegeLevel).to.be.oneOf([ + 'owner', + 'readAndWrite', + 'readOnly' + ]), + cb + ) + ], + callback + ) + +const expect_content_write_access = (user, project_id, callback) => + try_content_access( + user, + project_id, + (response, body) => + expect(body.privilegeLevel).to.be.oneOf(['owner', 'readAndWrite']), + callback + ) + +const expect_settings_write_access = (user, project_id, callback) => + try_settings_write_access( + user, + project_id, + (response, body) => expect(response.statusCode).to.be.oneOf([200, 204]), + callback + ) + +const expect_admin_access = (user, project_id, callback) => + try_admin_access( + user, + project_id, + (response, body) => expect(response.statusCode).to.be.oneOf([200, 204]), + callback + ) + +const expect_no_read_access = (user, project_id, options, callback) => + async.series( + [ + cb => + try_read_access( + user, + project_id, + function(response, body) { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.match( + new RegExp(options.redirect_to) + ) + }, + cb + ), + cb => + try_content_access( + user, + project_id, + (response, body) => expect(body.privilegeLevel).to.be.equal(false), + cb + ) + ], + callback + ) + +const expect_no_content_write_access = (user, project_id, callback) => + try_content_access( + user, + project_id, + (response, body) => + expect(body.privilegeLevel).to.be.oneOf([false, 'readOnly']), + callback + ) + +const expect_no_settings_write_access = (user, project_id, options, callback) => + try_settings_write_access( + user, + project_id, + function(response, body) { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.match( + new RegExp(options.redirect_to) + ) + }, + callback + ) + +const expect_no_admin_access = (user, project_id, options, callback) => + try_admin_access( + user, + project_id, + function(response, body) { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.match( + new RegExp(options.redirect_to) + ) + }, + callback + ) + +describe('Authorization', function() { + before(function(done) { + this.timeout(90000) + this.owner = new User() + this.other1 = new User() + this.other2 = new User() + this.anon = new User() + this.site_admin = new User({ email: 'admin@example.com' }) + return async.parallel( + [ + cb => this.owner.login(cb), + cb => this.other1.login(cb), + cb => this.other2.login(cb), + cb => this.anon.getCsrfToken(cb), + cb => { + return this.site_admin.login(err => { + if (typeof error !== 'undefined' && error !== null) { + return cb(err) + } + return this.site_admin.ensure_admin(cb) + }) + } + ], + done + ) + }) + + describe('private project', function() { + before(function(done) { + return this.owner.createProject( + 'private-project', + (error, project_id) => { + if (error != null) { + return done(error) + } + this.project_id = project_id + return done() + } + ) + }) + + it('should allow the owner read access to it', function(done) { + return expect_read_access(this.owner, this.project_id, done) + }) + + it('should allow the owner write access to its content', function(done) { + return expect_content_write_access(this.owner, this.project_id, done) + }) + + it('should allow the owner write access to its settings', function(done) { + return expect_settings_write_access(this.owner, this.project_id, done) + }) + + it('should allow the owner admin access to it', function(done) { + return expect_admin_access(this.owner, this.project_id, done) + }) + + it('should not allow another user read access to the project', function(done) { + return expect_no_read_access( + this.other1, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow another user write access to its content', function(done) { + return expect_no_content_write_access(this.other1, this.project_id, done) + }) + + it('should not allow another user write access to its settings', function(done) { + return expect_no_settings_write_access( + this.other1, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow another user admin access to it', function(done) { + return expect_no_admin_access( + this.other1, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow anonymous user read access to it', function(done) { + return expect_no_read_access( + this.anon, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow anonymous user write access to its content', function(done) { + return expect_no_content_write_access(this.anon, this.project_id, done) + }) + + it('should not allow anonymous user write access to its settings', function(done) { + return expect_no_settings_write_access( + this.anon, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow anonymous user admin access to it', function(done) { + return expect_no_admin_access( + this.anon, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should allow site admin users read access to it', function(done) { + return expect_read_access(this.site_admin, this.project_id, done) + }) + + it('should allow site admin users write access to its content', function(done) { + return expect_content_write_access(this.site_admin, this.project_id, done) + }) + + it('should allow site admin users write access to its settings', function(done) { + return expect_settings_write_access( + this.site_admin, + this.project_id, + done + ) + }) + + return it('should allow site admin users admin access to it', function(done) { + return expect_admin_access(this.site_admin, this.project_id, done) + }) + }) + + describe('shared project', function() { + before(function(done) { + this.rw_user = this.other1 + this.ro_user = this.other2 + return this.owner.createProject( + 'private-project', + (error, project_id) => { + if (error != null) { + return done(error) + } + this.project_id = project_id + return this.owner.addUserToProject( + this.project_id, + this.ro_user, + 'readOnly', + error => { + if (error != null) { + return done(error) + } + return this.owner.addUserToProject( + this.project_id, + this.rw_user, + 'readAndWrite', + error => { + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + } + ) + }) + + it('should allow the read-only user read access to it', function(done) { + return expect_read_access(this.ro_user, this.project_id, done) + }) + + it('should not allow the read-only user write access to its content', function(done) { + return expect_no_content_write_access(this.ro_user, this.project_id, done) + }) + + it('should not allow the read-only user write access to its settings', function(done) { + return expect_no_settings_write_access( + this.ro_user, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow the read-only user admin access to it', function(done) { + return expect_no_admin_access( + this.ro_user, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should allow the read-write user read access to it', function(done) { + return expect_read_access(this.rw_user, this.project_id, done) + }) + + it('should allow the read-write user write access to its content', function(done) { + return expect_content_write_access(this.rw_user, this.project_id, done) + }) + + it('should allow the read-write user write access to its settings', function(done) { + return expect_settings_write_access(this.rw_user, this.project_id, done) + }) + + return it('should not allow the read-write user admin access to it', function(done) { + return expect_no_admin_access( + this.rw_user, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + }) + + describe('public read-write project', function() { + before(function(done) { + return this.owner.createProject( + 'public-rw-project', + (error, project_id) => { + if (error != null) { + return done(error) + } + this.project_id = project_id + return this.owner.makePublic(this.project_id, 'readAndWrite', done) + } + ) + }) + + it('should allow a user read access to it', function(done) { + return expect_read_access(this.other1, this.project_id, done) + }) + + it('should allow a user write access to its content', function(done) { + return expect_content_write_access(this.other1, this.project_id, done) + }) + + it('should not allow a user write access to its settings', function(done) { + return expect_no_settings_write_access( + this.other1, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow a user admin access to it', function(done) { + return expect_no_admin_access( + this.other1, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should allow an anonymous user read access to it', function(done) { + return expect_read_access(this.anon, this.project_id, done) + }) + + it('should allow an anonymous user write access to its content', function(done) { + return expect_content_write_access(this.anon, this.project_id, done) + }) + + it('should not allow an anonymous user write access to its settings', function(done) { + return expect_no_settings_write_access( + this.anon, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + return it('should not allow an anonymous user admin access to it', function(done) { + return expect_no_admin_access( + this.anon, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + }) + + return describe('public read-only project', function() { + before(function(done) { + return this.owner.createProject( + 'public-ro-project', + (error, project_id) => { + if (error != null) { + return done(error) + } + this.project_id = project_id + return this.owner.makePublic(this.project_id, 'readOnly', done) + } + ) + }) + + it('should allow a user read access to it', function(done) { + return expect_read_access(this.other1, this.project_id, done) + }) + + it('should not allow a user write access to its content', function(done) { + return expect_no_content_write_access(this.other1, this.project_id, done) + }) + + it('should not allow a user write access to its settings', function(done) { + return expect_no_settings_write_access( + this.other1, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should not allow a user admin access to it', function(done) { + return expect_no_admin_access( + this.other1, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + it('should allow an anonymous user read access to it', function(done) { + return expect_read_access(this.anon, this.project_id, done) + }) + + it('should not allow an anonymous user write access to its content', function(done) { + return expect_no_content_write_access(this.anon, this.project_id, done) + }) + + it('should not allow an anonymous user write access to its settings', function(done) { + return expect_no_settings_write_access( + this.anon, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + + return it('should not allow an anonymous user admin access to it', function(done) { + return expect_no_admin_access( + this.anon, + this.project_id, + { redirect_to: '/restricted' }, + done + ) + }) + }) +}) diff --git a/services/web/test/acceptance/src/CloseSiteTests.js b/services/web/test/acceptance/src/CloseSiteTests.js new file mode 100644 index 0000000000..0a6e260f5a --- /dev/null +++ b/services/web/test/acceptance/src/CloseSiteTests.js @@ -0,0 +1,36 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Settings = require('settings-sharelatex') +const chai = require('chai') +const request = require('./helpers/request') + +describe('siteIsOpen', function() { + describe('when siteIsOpen is default (true)', () => + it('should get page', done => + request.get('/login', function(error, response, body) { + response.statusCode.should.equal(200) + return done() + }))) + + return describe('when siteIsOpen is false', function() { + beforeEach(() => (Settings.siteIsOpen = false)) + + afterEach(() => (Settings.siteIsOpen = true)) + + return it('should return maintenance page', done => + request.get('/login', function(error, response) { + response.statusCode.should.equal(503) + return done() + })) + }) +}) diff --git a/services/web/test/acceptance/src/ExportsTests.js b/services/web/test/acceptance/src/ExportsTests.js new file mode 100644 index 0000000000..a6d438d7ea --- /dev/null +++ b/services/web/test/acceptance/src/ExportsTests.js @@ -0,0 +1,109 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const request = require('./helpers/request') +const _ = require('underscore') + +const User = require('./helpers/User') +const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js') +const ExportsHandler = require('../../../app/src/Features/Exports/ExportsHandler.js') + +const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi') +const MockV1Api = require('./helpers/MockV1Api') + +describe('Exports', function() { + before(function(done) { + this.brand_variation_id = '18' + this.owner = new User() + return this.owner.login(error => { + if (error != null) { + throw error + } + return this.owner.createProject( + 'example-project', + { template: 'example' }, + (error, project_id) => { + this.project_id = project_id + if (error != null) { + throw error + } + return done() + } + ) + }) + }) + + return describe('exporting a project', function() { + beforeEach(function(done) { + this.version = Math.floor(Math.random() * 10000) + MockProjectHistoryApi.setProjectVersion(this.project_id, this.version) + this.export_id = Math.floor(Math.random() * 10000) + MockV1Api.setExportId(this.export_id) + MockV1Api.clearExportParams() + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/export/${this.brand_variation_id}`, + json: true, + body: { + title: 'title', + description: 'description', + author: 'author', + license: 'other', + showSource: true + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + this.exportResponseBody = body + return done() + } + ) + }) + + it('should have sent correct data to v1', function(done) { + const { + project, + user, + destination, + options + } = MockV1Api.getLastExportParams() + // project details should match + expect(project.id).to.equal(this.project_id) + expect(project.rootDocPath).to.equal('/main.tex') + // gallery details should match + expect(project.metadata.title).to.equal('title') + expect(project.metadata.description).to.equal('description') + expect(project.metadata.author).to.equal('author') + expect(project.metadata.license).to.equal('other') + expect(project.metadata.showSource).to.equal(true) + // version should match what was retrieved from project-history + expect(project.historyVersion).to.equal(this.version) + // user details should match + expect(user.id).to.equal(this.owner.id) + expect(user.email).to.equal(this.owner.email) + // brand-variation should match + expect(destination.brandVariationId).to.equal(this.brand_variation_id) + return done() + }) + + return it('should have returned the export ID provided by v1', function(done) { + expect(this.exportResponseBody.export_v1_id).to.equal(this.export_id) + return done() + }) + }) +}) diff --git a/services/web/test/acceptance/src/FeatureUpdaterTests.js b/services/web/test/acceptance/src/FeatureUpdaterTests.js new file mode 100644 index 0000000000..51684bb9c0 --- /dev/null +++ b/services/web/test/acceptance/src/FeatureUpdaterTests.js @@ -0,0 +1,344 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const UserClient = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') +const { ObjectId } = require('../../../app/src/infrastructure/mongojs') +const { Subscription } = require('../../../app/src/models/Subscription') +const { User } = require('../../../app/src/models/User') +const FeaturesUpdater = require('../../../app/src/Features/Subscription/FeaturesUpdater') + +const MockV1Api = require('./helpers/MockV1Api') +const logger = require('logger-sharelatex') +logger.logger.level('error') + +const syncUserAndGetFeatures = function(user, callback) { + if (callback == null) { + callback = function(error, features) {} + } + return FeaturesUpdater.refreshFeatures(user._id, false, function(error) { + if (error != null) { + return callback(error) + } + return User.findById(user._id, function(error, user) { + if (error != null) { + return callback(error) + } + const { features } = user.toObject() + delete features.$init // mongoose internals + return callback(null, features) + }) + }) +} + +describe('FeatureUpdater.refreshFeatures', function() { + beforeEach(function(done) { + this.user = new UserClient() + return this.user.ensureUserExists(function(error) { + if (error != null) { + throw error + } + return done() + }) + }) + + describe('when user has no subscriptions', () => + it('should set their features to the basic set', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + expect(features).to.deep.equal(settings.defaultFeatures) + return done() + }) + })) + + describe('when the user has an individual subscription', function() { + beforeEach(function() { + return Subscription.create({ + admin_id: this.user._id, + manager_ids: [this.user._id], + planCode: 'collaborator', + customAccount: true + }) + }) // returns a promise + + return it('should set their features to the upgraded set', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + const plan = settings.plans.find( + plan => plan.planCode === 'collaborator' + ) + expect(features).to.deep.equal(plan.features) + return done() + }) + }) + }) + + describe('when the user is in a group subscription', function() { + beforeEach(function() { + return Subscription.create({ + admin_id: ObjectId(), + member_ids: [this.user._id], + groupAccount: true, + planCode: 'collaborator', + customAccount: true + }) + }) // returns a promise + + return it('should set their features to the upgraded set', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + const plan = settings.plans.find( + plan => plan.planCode === 'collaborator' + ) + expect(features).to.deep.equal(plan.features) + return done() + }) + }) + }) + + describe('when the user has bonus features', function() { + beforeEach(function() { + return User.update( + { + _id: this.user._id + }, + { + refered_user_count: 10 + } + ) + }) // returns a promise + + return it('should set their features to the bonus set', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + expect(features).to.deep.equal( + Object.assign( + {}, + settings.defaultFeatures, + settings.bonus_features[9] + ) + ) + return done() + }) + }) + }) + + describe('when the user has affiliations', function() { + beforeEach(function() { + this.institutionPlan = settings.plans.find( + plan => plan.planCode === settings.institutionPlanCode + ) + this.email = this.user.emails[0].email + return (this.affiliationData = { + email: this.email, + institution: { licence: 'pro_plus', confirmed: true } + }) + }) + + it('should not set their features if email is not confirmed', function(done) { + MockV1Api.setAffiliations([this.affiliationData]) + return syncUserAndGetFeatures(this.user, (error, features) => { + expect(features).to.deep.equal(settings.defaultFeatures) + return done() + }) + }) + + it('should set their features if email is confirmed', function(done) { + MockV1Api.setAffiliations([this.affiliationData]) + return this.user.confirmEmail(this.email, error => { + return syncUserAndGetFeatures(this.user, (error, features) => { + expect(features).to.deep.equal(this.institutionPlan.features) + return done() + }) + }) + }) + + return it('should not set their features if institution is not confirmed', function(done) { + this.affiliationData.institution.confirmed = false + MockV1Api.setAffiliations([this.affiliationData]) + return this.user.confirmEmail(this.email, error => { + return syncUserAndGetFeatures(this.user, (error, features) => { + expect(features).to.deep.equal(settings.defaultFeatures) + return done() + }) + }) + }) + }) + + describe('when the user is due bonus features and has extra features that no longer apply', function() { + beforeEach(function() { + return User.update( + { + _id: this.user._id + }, + { + refered_user_count: 10, + 'features.github': true + } + ) + }) // returns a promise + + return it('should set their features to the bonus set and downgrade the extras', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + expect(features).to.deep.equal( + Object.assign( + {}, + settings.defaultFeatures, + settings.bonus_features[9] + ) + ) + return done() + }) + }) + }) + + describe('when the user has a v1 plan', function() { + beforeEach(function() { + MockV1Api.setUser(42, { plan_name: 'free' }) + return User.update( + { + _id: this.user._id + }, + { + overleaf: { + id: 42 + } + } + ) + }) // returns a promise + + return it('should set their features to the v1 plan', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + const plan = settings.plans.find(plan => plan.planCode === 'v1_free') + expect(features).to.deep.equal(plan.features) + return done() + }) + }) + }) + + describe('when the user has a v1 plan and bonus features', function() { + beforeEach(function() { + MockV1Api.setUser(42, { plan_name: 'free' }) + return User.update( + { + _id: this.user._id + }, + { + overleaf: { + id: 42 + }, + refered_user_count: 10 + } + ) + }) // returns a promise + + return it('should set their features to the best of the v1 plan and bonus features', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + const v1plan = settings.plans.find(plan => plan.planCode === 'v1_free') + const expectedFeatures = Object.assign( + {}, + v1plan.features, + settings.bonus_features[9] + ) + expect(features).to.deep.equal(expectedFeatures) + return done() + }) + }) + }) + + describe('when the user has a group and personal subscription', function() { + beforeEach(function(done) { + Subscription.create( + { + admin_id: this.user._id, + manager_ids: [this.user._id], + planCode: 'professional', + customAccount: true + }, + error => { + if (error != null) { + throw error + } + return Subscription.create( + { + admin_id: ObjectId(), + member_ids: [this.user._id], + groupAccount: true, + planCode: 'collaborator', + customAccount: true + }, + done + ) + } + ) + }) + + return it('should set their features to the best set', function(done) { + return syncUserAndGetFeatures(this.user, (error, features) => { + if (error != null) { + throw error + } + const plan = settings.plans.find( + plan => plan.planCode === 'professional' + ) + expect(features).to.deep.equal(plan.features) + return done() + }) + }) + }) + + return describe('when the notifyV1Flag is passed', function() { + beforeEach(function() { + return User.update( + { + _id: this.user._id + }, + { + overleaf: { + id: 42 + } + } + ) + }) // returns a promise + + return it('should ping the v1 API end point to sync', function(done) { + return FeaturesUpdater.refreshFeatures(this.user._id, true, error => { + return setTimeout(() => { + expect(MockV1Api.syncUserFeatures.calledWith('42')).to.equal(true) + return done() + }, 500) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/HistoryTests.js b/services/web/test/acceptance/src/HistoryTests.js new file mode 100644 index 0000000000..7bffe4cd89 --- /dev/null +++ b/services/web/test/acceptance/src/HistoryTests.js @@ -0,0 +1,108 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') + +const { db, ObjectId } = require('../../../app/src/infrastructure/mongojs') +const MockV1HistoryApi = require('./helpers/MockV1HistoryApi') +const User = require('./helpers/User') + +describe('History', function() { + beforeEach(function(done) { + this.owner = new User() + return this.owner.login(done) + }) + + return describe('zip download of version', function() { + it('should stream the zip file of a version', function(done) { + return this.owner.createProject( + 'example-project', + (error, project_id) => { + this.project_id = project_id + if (error != null) { + return done(error) + } + this.v1_history_id = 42 + return db.projects.update( + { + _id: ObjectId(this.project_id) + }, + { + $set: { + 'overleaf.history.id': this.v1_history_id + } + }, + error => { + if (error != null) { + return done(error) + } + return this.owner.request( + `/project/${this.project_id}/version/42/zip`, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(200) + expect(response.headers['content-type']).to.equal( + 'application/zip' + ) + expect(response.headers['content-disposition']).to.equal( + 'attachment; filename="example-project%20(Version%2042).zip"' + ) + expect(body).to.equal( + `Mock zip for ${this.v1_history_id} at version 42` + ) + return done() + } + ) + } + ) + } + ) + }) + + return it('should return 402 for non-v2-history project', function(done) { + return this.owner.createProject('non-v2-project', (error, project_id) => { + this.project_id = project_id + if (error != null) { + return done(error) + } + return db.projects.update( + { + _id: ObjectId(this.project_id) + }, + { + $unset: { + 'overleaf.history.id': true + } + }, + error => { + if (error != null) { + return done(error) + } + return this.owner.request( + `/project/${this.project_id}/version/42/zip`, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(402) + return done() + } + ) + } + ) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/Init.js b/services/web/test/acceptance/src/Init.js new file mode 100644 index 0000000000..15d4d6fa54 --- /dev/null +++ b/services/web/test/acceptance/src/Init.js @@ -0,0 +1,11 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const App = require('../../../app.js') +require('logger-sharelatex').logger.level('error') + +before(done => App.listen(3000, 'localhost', done)) diff --git a/services/web/test/acceptance/src/LabelsTests.js b/services/web/test/acceptance/src/LabelsTests.js new file mode 100644 index 0000000000..5a7bc7efbf --- /dev/null +++ b/services/web/test/acceptance/src/LabelsTests.js @@ -0,0 +1,124 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require('underscore') +const { expect } = require('chai') +const { ObjectId } = require('mongojs') +const request = require('./helpers/request') + +const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi') +const User = require('./helpers/User') + +describe('Labels', function() { + beforeEach(function(done) { + this.owner = new User() + return this.owner.login(error => { + if (error != null) { + throw error + } + return this.owner.createProject( + 'example-project', + { template: 'example' }, + (error, project_id) => { + this.project_id = project_id + if (error != null) { + throw error + } + return done() + } + ) + }) + }) + + afterEach(() => MockProjectHistoryApi.reset()) + + it('getting labels', function(done) { + const label_id = new ObjectId().toString() + const comment = 'a label comment' + const version = 3 + MockProjectHistoryApi.addLabel(this.project_id, { + id: label_id, + comment, + version + }) + + return this.owner.request( + { + method: 'GET', + url: `/project/${this.project_id}/labels`, + json: true + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + expect(body).to.deep.equal([{ id: label_id, comment, version }]) + return done() + } + ) + }) + + it('creating a label', function(done) { + const comment = 'a label comment' + const version = 3 + + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/labels`, + json: { comment, version } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + const { label_id } = body + expect(MockProjectHistoryApi.getLabels(this.project_id)).to.deep.equal([ + { id: label_id, comment, version } + ]) + return done() + } + ) + }) + + return it('deleting a label', function(done) { + const label_id = new ObjectId().toString() + const comment = 'a label comment' + const version = 3 + MockProjectHistoryApi.addLabel(this.project_id, { + id: label_id, + comment, + version + }) + + return this.owner.request( + { + method: 'DELETE', + url: `/project/${this.project_id}/labels/${label_id}`, + json: true + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(204) + expect(MockProjectHistoryApi.getLabels(this.project_id)).to.deep.equal( + [] + ) + return done() + } + ) + }) +}) diff --git a/services/web/test/acceptance/src/LinkedFilesTests.js b/services/web/test/acceptance/src/LinkedFilesTests.js new file mode 100644 index 0000000000..82ec927f47 --- /dev/null +++ b/services/web/test/acceptance/src/LinkedFilesTests.js @@ -0,0 +1,753 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async') +const { expect } = require('chai') +const _ = require('underscore') +const mkdirp = require('mkdirp') + +const Settings = require('settings-sharelatex') +const MockFileStoreApi = require('./helpers/MockFileStoreApi') +const request = require('./helpers/request') +const User = require('./helpers/User') + +const MockClsiApi = require('./helpers/MockClsiApi') + +const express = require('express') +const LinkedUrlProxy = express() +LinkedUrlProxy.get('/', (req, res, next) => { + if (req.query.url === 'http://example.com/foo') { + return res.send('foo foo foo') + } else if (req.query.url === 'http://example.com/bar') { + return res.send('bar bar bar') + } else { + return res.sendStatus(404) + } +}) + +describe('LinkedFiles', function() { + before(function(done) { + return LinkedUrlProxy.listen(6543, error => { + if (error != null) { + return done(error) + } + this.owner = new User() + return this.owner.login(() => mkdirp(Settings.path.dumpFolder, done)) + }) + }) + + describe('creating a project linked file', function() { + before(function(done) { + this.source_doc_name = 'test.txt' + return async.series( + [ + cb => { + return this.owner.createProject( + 'plf-test-one', + { template: 'blank' }, + (error, project_id) => { + this.project_one_id = project_id + return cb(error) + } + ) + }, + cb => { + return this.owner.getProject( + this.project_one_id, + (error, project) => { + this.project_one = project + this.project_one_root_folder_id = project.rootFolder[0]._id.toString() + return cb(error) + } + ) + }, + cb => { + return this.owner.createProject( + 'plf-test-two', + { template: 'blank' }, + (error, project_id) => { + this.project_two_id = project_id + return cb(error) + } + ) + }, + cb => { + return this.owner.getProject( + this.project_two_id, + (error, project) => { + this.project_two = project + this.project_two_root_folder_id = project.rootFolder[0]._id.toString() + return cb(error) + } + ) + }, + cb => { + return this.owner.createDocInProject( + this.project_two_id, + this.project_two_root_folder_id, + this.source_doc_name, + (error, doc_id) => { + this.source_doc_id = doc_id + return cb(error) + } + ) + }, + cb => { + return this.owner.createDocInProject( + this.project_two_id, + this.project_two_root_folder_id, + 'some-harmless-doc.txt', + (error, doc_id) => { + return cb(error) + } + ) + } + ], + done + ) + }) + + it('should produce a list of the users projects', function(done) { + return this.owner.request.get( + { + url: '/user/projects', + json: true + }, + (err, response, body) => { + expect(err).to.not.exist + expect(body).to.deep.equal({ + projects: [ + { + _id: this.project_one_id, + name: 'plf-test-one', + accessLevel: 'owner' + }, + { + _id: this.project_two_id, + name: 'plf-test-two', + accessLevel: 'owner' + } + ] + }) + return done() + } + ) + }) + + it('should produce a list of entities in the project', function(done) { + return this.owner.request.get( + { + url: `/project/${this.project_two_id}/entities`, + json: true + }, + (err, response, body) => { + expect(err).to.not.exist + expect(body).to.deep.equal({ + project_id: this.project_two_id, + entities: [ + { path: '/main.tex', type: 'doc' }, + { path: '/some-harmless-doc.txt', type: 'doc' }, + { path: '/test.txt', type: 'doc' } + ] + }) + return done() + } + ) + }) + + it('should import a file from the source project', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_one_id}/linked_file`, + json: { + name: 'test-link.txt', + parent_folder_id: this.project_one_root_folder_id, + provider: 'project_file', + data: { + source_project_id: this.project_two_id, + source_entity_path: `/${this.source_doc_name}` + } + } + }, + (error, response, body) => { + expect(response.statusCode).to.equal(200) + const { new_file_id } = body + this.existing_file_id = new_file_id + expect(new_file_id).to.exist + return this.owner.getProject( + this.project_one_id, + (error, project) => { + if (error != null) { + return done(error) + } + const firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.linkedFileData).to.deep.equal({ + provider: 'project_file', + source_project_id: this.project_two_id, + source_entity_path: `/${this.source_doc_name}` + }) + expect(firstFile.name).to.equal('test-link.txt') + return done() + } + ) + } + ) + }) + + it('should refresh the file', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_one_id}/linked_file/${ + this.existing_file_id + }/refresh`, + json: true + }, + (error, response, body) => { + expect(response.statusCode).to.equal(200) + const { new_file_id } = body + expect(new_file_id).to.exist + expect(new_file_id).to.not.equal(this.existing_file_id) + this.refreshed_file_id = new_file_id + return this.owner.getProject( + this.project_one_id, + (error, project) => { + if (error != null) { + return done(error) + } + const firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.name).to.equal('test-link.txt') + return done() + } + ) + } + ) + }) + + return it('should not allow to create a linked-file with v1 id', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_one_id}/linked_file`, + json: { + name: 'test-link-should-not-work.txt', + parent_folder_id: this.project_one_root_folder_id, + provider: 'project_file', + data: { + v1_source_doc_id: 1234, + source_entity_path: `/${this.source_doc_name}` + } + } + }, + (error, response, body) => { + expect(response.statusCode).to.equal(403) + expect(body).to.equal('You do not have access to this project') + return done() + } + ) + }) + }) + + describe('with a linked project_file from a v1 project that has not been imported', function() { + before(function(done) { + return async.series( + [ + cb => { + return this.owner.createProject( + 'plf-v1-test-one', + { template: 'blank' }, + (error, project_id) => { + this.project_one_id = project_id + return cb(error) + } + ) + }, + cb => { + return this.owner.getProject( + this.project_one_id, + (error, project) => { + this.project_one = project + this.project_one_root_folder_id = project.rootFolder[0]._id.toString() + this.project_one.rootFolder[0].fileRefs.push({ + linkedFileData: { + provider: 'project_file', + v1_source_doc_id: 9999999, // We won't find this id in the database + source_entity_path: 'example.jpeg' + }, + _id: 'abcd', + rev: 0, + created: new Date(), + name: 'example.jpeg' + }) + return this.owner.saveProject(this.project_one, cb) + } + ) + } + ], + done + ) + }) + + return it('should refuse to refresh', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_one_id}/linked_file/abcd/refresh`, + json: true + }, + (error, response, body) => { + expect(response.statusCode).to.equal(409) + expect(body).to.equal( + 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file' + ) + return done() + } + ) + }) + }) + + describe('creating a URL based linked file', function() { + before(function(done) { + return this.owner.createProject( + 'url-linked-files-project', + { template: 'blank' }, + (error, project_id) => { + if (error != null) { + throw error + } + this.project_id = project_id + return this.owner.getProject(project_id, (error, project) => { + if (error != null) { + throw error + } + this.project = project + this.root_folder_id = project.rootFolder[0]._id.toString() + return done() + }) + } + ) + }) + + it('should download the URL and create a file with the contents and linkedFileData', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_id}/linked_file`, + json: { + provider: 'url', + data: { + url: 'http://example.com/foo' + }, + parent_folder_id: this.root_folder_id, + name: 'url-test-file-1' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + const file = project.rootFolder[0].fileRefs[0] + expect(file.linkedFileData).to.deep.equal({ + provider: 'url', + url: 'http://example.com/foo' + }) + return this.owner.request.get( + `/project/${this.project_id}/file/${file._id}`, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + expect(body).to.equal('foo foo foo') + return done() + } + ) + }) + } + ) + }) + + it('should replace and update a URL based linked file', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_id}/linked_file`, + json: { + provider: 'url', + data: { + url: 'http://example.com/foo' + }, + parent_folder_id: this.root_folder_id, + name: 'url-test-file-2' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return this.owner.request.post( + { + url: `/project/${this.project_id}/linked_file`, + json: { + provider: 'url', + data: { + url: 'http://example.com/bar' + }, + parent_folder_id: this.root_folder_id, + name: 'url-test-file-2' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return this.owner.getProject( + this.project_id, + (error, project) => { + if (error != null) { + throw error + } + const file = project.rootFolder[0].fileRefs[1] + expect(file.linkedFileData).to.deep.equal({ + provider: 'url', + url: 'http://example.com/bar' + }) + return this.owner.request.get( + `/project/${this.project_id}/file/${file._id}`, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + expect(body).to.equal('bar bar bar') + return done() + } + ) + } + ) + } + ) + } + ) + }) + + it('should return an error if the URL does not succeed', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_id}/linked_file`, + json: { + provider: 'url', + data: { + url: 'http://example.com/does-not-exist' + }, + parent_folder_id: this.root_folder_id, + name: 'url-test-file-3' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(422) // unprocessable + expect(body).to.equal( + 'Your URL could not be reached (404 status code). Please check it and try again.' + ) + return done() + } + ) + }) + + it('should return an error if the URL is invalid', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_id}/linked_file`, + json: { + provider: 'url', + data: { + url: '!^$%' + }, + parent_folder_id: this.root_folder_id, + name: 'url-test-file-4' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(422) // unprocessable + expect(body).to.equal( + 'Your URL is not valid. Please check it and try again.' + ) + return done() + } + ) + }) + + it('should return an error if the URL uses a non-http protocol', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_id}/linked_file`, + json: { + provider: 'url', + data: { + url: 'ftp://localhost' + }, + parent_folder_id: this.root_folder_id, + name: 'url-test-file-5' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(422) // unprocessable + expect(body).to.equal( + 'Your URL is not valid. Please check it and try again.' + ) + return done() + } + ) + }) + + return it('should accept a URL withuot a leading http://, and add it', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_id}/linked_file`, + json: { + provider: 'url', + data: { + url: 'example.com/foo' + }, + parent_folder_id: this.root_folder_id, + name: 'url-test-file-6' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + const file = _.find( + project.rootFolder[0].fileRefs, + file => file.name === 'url-test-file-6' + ) + expect(file.linkedFileData).to.deep.equal({ + provider: 'url', + url: 'http://example.com/foo' + }) + return this.owner.request.get( + `/project/${this.project_id}/file/${file._id}`, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + expect(body).to.equal('foo foo foo') + return done() + } + ) + }) + } + ) + }) + }) + + // TODO: Add test for asking for host that return ENOTFOUND + // (This will probably end up handled by the proxy) + + describe('creating a linked output file', function() { + before(function(done) { + return async.series( + [ + cb => { + return this.owner.createProject( + 'output-test-one', + { template: 'blank' }, + (error, project_id) => { + this.project_one_id = project_id + return cb(error) + } + ) + }, + cb => { + return this.owner.getProject( + this.project_one_id, + (error, project) => { + this.project_one = project + this.project_one_root_folder_id = project.rootFolder[0]._id.toString() + return cb(error) + } + ) + }, + cb => { + return this.owner.createProject( + 'output-test-two', + { template: 'blank' }, + (error, project_id) => { + this.project_two_id = project_id + return cb(error) + } + ) + }, + cb => { + return this.owner.getProject( + this.project_two_id, + (error, project) => { + this.project_two = project + this.project_two_root_folder_id = project.rootFolder[0]._id.toString() + return cb(error) + } + ) + } + ], + done + ) + }) + + it('should import the project.pdf file from the source project', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_one_id}/linked_file`, + json: { + name: 'test.pdf', + parent_folder_id: this.project_one_root_folder_id, + provider: 'project_output_file', + data: { + source_project_id: this.project_two_id, + source_output_file_path: 'project.pdf', + build_id: '1234-abcd' + } + } + }, + (error, response, body) => { + const { new_file_id } = body + this.existing_file_id = new_file_id + expect(new_file_id).to.exist + return this.owner.getProject( + this.project_one_id, + (error, project) => { + if (error != null) { + return done(error) + } + const firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.linkedFileData).to.deep.equal({ + provider: 'project_output_file', + source_project_id: this.project_two_id, + source_output_file_path: 'project.pdf', + build_id: '1234-abcd' + }) + expect(firstFile.name).to.equal('test.pdf') + return done() + } + ) + } + ) + }) + + return it('should refresh the file', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_one_id}/linked_file/${ + this.existing_file_id + }/refresh`, + json: true + }, + (error, response, body) => { + const { new_file_id } = body + expect(new_file_id).to.exist + expect(new_file_id).to.not.equal(this.existing_file_id) + this.refreshed_file_id = new_file_id + return this.owner.getProject( + this.project_one_id, + (error, project) => { + if (error != null) { + return done(error) + } + const firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.name).to.equal('test.pdf') + return done() + } + ) + } + ) + }) + }) + + return describe('with a linked project_output_file from a v1 project that has not been imported', function() { + before(function(done) { + return async.series( + [ + cb => { + return this.owner.createProject( + 'output-v1-test-one', + { template: 'blank' }, + (error, project_id) => { + this.project_one_id = project_id + return cb(error) + } + ) + }, + cb => { + return this.owner.getProject( + this.project_one_id, + (error, project) => { + this.project_one = project + this.project_one_root_folder_id = project.rootFolder[0]._id.toString() + this.project_one.rootFolder[0].fileRefs.push({ + linkedFileData: { + provider: 'project_output_file', + v1_source_doc_id: 9999999, // We won't find this id in the database + source_output_file_path: 'project.pdf' + }, + _id: 'abcdef', + rev: 0, + created: new Date(), + name: 'whatever.pdf' + }) + return this.owner.saveProject(this.project_one, cb) + } + ) + } + ], + done + ) + }) + + return it('should refuse to refresh', function(done) { + return this.owner.request.post( + { + url: `/project/${this.project_one_id}/linked_file/abcdef/refresh`, + json: true + }, + (error, response, body) => { + expect(response.statusCode).to.equal(409) + expect(body).to.equal( + 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file' + ) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/acceptance/src/ProjectCRUDTests.js b/services/web/test/acceptance/src/ProjectCRUDTests.js new file mode 100644 index 0000000000..048d3de052 --- /dev/null +++ b/services/web/test/acceptance/src/ProjectCRUDTests.js @@ -0,0 +1,41 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const User = require('./helpers/User') + +describe('Project CRUD', function() { + before(function(done) { + this.user = new User() + return this.user.login(done) + }) + + describe("when project doesn't exist", () => + it('should return 404', function(done) { + return this.user.request.get( + '/project/aaaaaaaaaaaaaaaaaaaaaaaa', + function(err, res, body) { + expect(res.statusCode).to.equal(404) + return done() + } + ) + })) + + return describe('when project has malformed id', () => + it('should return 404', function(done) { + return this.user.request.get('/project/blah', function(err, res, body) { + expect(res.statusCode).to.equal(404) + return done() + }) + })) +}) diff --git a/services/web/test/acceptance/src/ProjectDuplicateNameTests.js b/services/web/test/acceptance/src/ProjectDuplicateNameTests.js new file mode 100644 index 0000000000..da3989cb16 --- /dev/null +++ b/services/web/test/acceptance/src/ProjectDuplicateNameTests.js @@ -0,0 +1,690 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + mocha/no-identical-title, + no-path-concat, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async') +const { expect } = require('chai') +const sinon = require('sinon') +const mkdirp = require('mkdirp') +const { ObjectId } = require('mongojs') +const Path = require('path') +const fs = require('fs') +const Settings = require('settings-sharelatex') +const _ = require('underscore') + +const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js') + +const MockDocStoreApi = require('./helpers/MockDocstoreApi') +const MockFileStoreApi = require('./helpers/MockFileStoreApi') +const request = require('./helpers/request') +const User = require('./helpers/User') + +describe('ProjectDuplicateNames', function() { + before(function(done) { + this.owner = new User() + this.owner.login(done) + this.project = {} + return (this.callback = sinon.stub()) + }) + + return describe('creating a project from the example template', function() { + before(function(done) { + return this.owner.createProject( + 'example-project', + { template: 'example' }, + (error, project_id) => { + if (error != null) { + throw error + } + this.example_project_id = project_id + return this.owner.getProject(project_id, (error, project) => { + this.project = project + this.mainTexDoc = _.find( + project.rootFolder[0].docs, + doc => doc.name === 'main.tex' + ) + this.refBibDoc = _.find( + project.rootFolder[0].docs, + doc => doc.name === 'references.bib' + ) + this.imageFile = _.find( + project.rootFolder[0].fileRefs, + file => file.name === 'universe.jpg' + ) + this.rootFolderId = project.rootFolder[0]._id.toString() + // create a folder called 'testfolder' + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder`, + json: { + name: 'testfolder', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.testFolderId = body._id + return done() + } + ) + }) + } + ) + }) + + it('should create a project', function() { + expect(this.project.rootFolder[0].docs.length).to.equal(2) + return expect(this.project.rootFolder[0].fileRefs.length).to.equal(1) + }) + + it('should create two docs in the docstore', function() { + const docs = MockDocStoreApi.docs[this.example_project_id] + return expect(Object.keys(docs).length).to.equal(2) + }) + + it('should create one file in the filestore', function() { + const files = MockFileStoreApi.files[this.example_project_id] + return expect(Object.keys(files).length).to.equal(1) + }) + + describe('for an existing doc', function() { + describe('trying to add a doc with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc`, + json: { + name: 'main.tex', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to add a folder with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder`, + json: { + name: 'main.tex', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + return describe('trying to add a folder with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder`, + json: { + name: 'main.tex', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + }) + + describe('for an existing file', function() { + describe('trying to add a doc with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc`, + json: { + name: 'universe.jpg', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to add a folder with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder`, + json: { + name: 'universe.jpg', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + return describe('trying to upload a file with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/upload`, + json: true, + qs: { + folder_id: this.rootFolderId, + qqfilename: 'universe.jpg' + }, + formData: { + qqfile: { + value: fs.createReadStream( + Path.resolve(__dirname + '/../files/1pixel.png') + ), + options: { + filename: 'universe.jpg', + contentType: 'image/jpeg' + } + } + } + }, + (err, res, body) => { + this.body = body + // update the image id because we have replaced the file + this.imageFile._id = this.body.entity_id + return done() + } + ) + }) + + return it('should succeed (overwriting the file)', function() { + return expect(this.body.success).to.equal(true) + }) + }) + }) + // at this point the @imageFile._id has changed + + describe('for an existing folder', function() { + describe('trying to add a doc with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc`, + json: { + name: 'testfolder', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to add a folder with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder`, + json: { + name: 'testfolder', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + return describe('trying to upload a file with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/upload`, + json: true, + qs: { + folder_id: this.rootFolderId, + qqfilename: 'universe.jpg' + }, + formData: { + qqfile: { + value: fs.createReadStream( + Path.resolve(__dirname + '/../files/1pixel.png') + ), + options: { + filename: 'testfolder', + contentType: 'image/jpeg' + } + } + } + }, + (err, res, body) => { + this.body = body + return done() + } + ) + }) + + return it('should respond with failure status', function() { + return expect(this.body.success).to.equal(false) + }) + }) + }) + + describe('for an existing doc', function() { + describe('trying to rename a doc to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc/${ + this.refBibDoc._id + }/rename`, + json: { + name: 'main.tex' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to rename a folder to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder/${ + this.testFolderId + }/rename`, + json: { + name: 'main.tex' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + return describe('trying to rename a file to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/file/${ + this.imageFile._id + }/rename`, + json: { + name: 'main.tex' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with failure status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + }) + + describe('for an existing file', function() { + describe('trying to rename a doc to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc/${ + this.refBibDoc._id + }/rename`, + json: { + name: 'universe.jpg' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to rename a folder to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder/${ + this.testFolderId + }/rename`, + json: { + name: 'universe.jpg' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + return describe('trying to rename a file to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/file/${ + this.imageFile._id + }/rename`, + json: { + name: 'universe.jpg' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with failure status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + }) + + describe('for an existing folder', function() { + describe('trying to rename a doc to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc/${ + this.refBibDoc._id + }/rename`, + json: { + name: 'testfolder' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to rename a folder to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder/${ + this.testFolderId + }/rename`, + json: { + name: 'testfolder' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + return describe('trying to rename a file to the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/file/${ + this.imageFile._id + }/rename`, + json: { + name: 'testfolder' + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with failure status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + }) + + return describe('for an existing folder with a file with the same name', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc`, + json: { + name: 'main.tex', + parent_folder_id: this.testFolderId + } + }, + (err, res, body) => { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc`, + json: { + name: 'universe.jpg', + parent_folder_id: this.testFolderId + } + }, + (err, res, body) => { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder`, + json: { + name: 'otherFolder', + parent_folder_id: this.testFolderId + } + }, + (err, res, body) => { + this.subFolderId = body._id + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder`, + json: { + name: 'otherFolder', + parent_folder_id: this.rootFolderId + } + }, + (err, res, body) => { + this.otherFolderId = body._id + return done() + } + ) + } + ) + } + ) + } + ) + }) + + describe('trying to move a doc into the folder', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/doc/${ + this.mainTexDoc._id + }/move`, + json: { + folder_id: this.testFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to move a file into the folder', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/file/${ + this.imageFile._id + }/move`, + json: { + folder_id: this.testFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + describe('trying to move a folder into the folder', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder/${ + this.otherFolderId + }/move`, + json: { + folder_id: this.testFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + + return describe('trying to move a folder into a subfolder of itself', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `/project/${this.example_project_id}/folder/${ + this.testFolderId + }/move`, + json: { + folder_id: this.subFolderId + } + }, + (err, res, body) => { + this.res = res + return done() + } + ) + }) + + return it('should respond with 400 error status', function() { + return expect(this.res.statusCode).to.equal(400) + }) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/ProjectFeaturesTests.js b/services/web/test/acceptance/src/ProjectFeaturesTests.js new file mode 100644 index 0000000000..8f8d7cb97e --- /dev/null +++ b/services/web/test/acceptance/src/ProjectFeaturesTests.js @@ -0,0 +1,103 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const User = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') + +const joinProject = (user_id, project_id, callback) => + request.post( + { + url: `/project/${project_id}/join`, + qs: { user_id }, + auth: { + user: settings.apis.web.user, + pass: settings.apis.web.pass, + sendImmediately: true + }, + json: true, + jar: false + }, + callback + ) + +describe('ProjectFeatures', function() { + before(function(done) { + this.timeout(90000) + this.owner = new User() + return async.series([cb => this.owner.login(cb)], done) + }) + + return describe('with private project', function() { + before(function(done) { + return this.owner.createProject( + 'private-project', + (error, project_id) => { + if (error != null) { + return done(error) + } + this.project_id = project_id + return done() + } + ) + }) + + describe('with an upgraded account', function() { + before(function(done) { + return this.owner.upgradeFeatures(done) + }) + after(function(done) { + return this.owner.defaultFeatures(done) + }) + + return it('should have premium features', function(done) { + return joinProject(this.owner._id, this.project_id, function( + error, + response, + body + ) { + expect(body.project.features.compileGroup).to.equal('priority') + expect(body.project.features.versioning).to.equal(true) + expect(body.project.features.templates).to.equal(true) + expect(body.project.features.dropbox).to.equal(true) + return done() + }) + }) + }) + + return describe('with an basic account', function() { + before(function(done) { + return this.owner.downgradeFeatures(done) + }) + after(function(done) { + return this.owner.defaultFeatures(done) + }) + + return it('should have basic features', function(done) { + return joinProject(this.owner._id, this.project_id, function( + error, + response, + body + ) { + expect(body.project.features.compileGroup).to.equal('standard') + expect(body.project.features.versioning).to.equal(false) + expect(body.project.features.templates).to.equal(false) + expect(body.project.features.dropbox).to.equal(false) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/ProjectInviteTests.js b/services/web/test/acceptance/src/ProjectInviteTests.js new file mode 100644 index 0000000000..5b05c374ba --- /dev/null +++ b/services/web/test/acceptance/src/ProjectInviteTests.js @@ -0,0 +1,962 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const Async = require('async') +const User = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') +const CollaboratorsEmailHandler = require('../../../app/src/Features/Collaborators/CollaboratorsEmailHandler') + +const createInvite = function(sendingUser, projectId, email, callback) { + if (callback == null) { + callback = function(err, invite) {} + } + return sendingUser.getCsrfToken(function(err) { + if (err) { + return callback(err) + } + return sendingUser.request.post( + { + uri: `/project/${projectId}/invite`, + json: { + email, + privileges: 'readAndWrite' + } + }, + function(err, response, body) { + if (err) { + return callback(err) + } + expect(response.statusCode).to.equal(200) + return callback(null, body.invite) + } + ) + }) +} + +const createProject = function(owner, projectName, callback) { + if (callback == null) { + callback = function(err, projectId, project) {} + } + return owner.createProject(projectName, function(err, projectId) { + if (err) { + throw err + } + const fakeProject = { + _id: projectId, + name: projectName, + owner_ref: owner + } + return callback(err, projectId, fakeProject) + }) +} + +const createProjectAndInvite = function(owner, projectName, email, callback) { + if (callback == null) { + callback = function(err, project, invite) {} + } + return createProject(owner, projectName, function(err, projectId, project) { + if (err) { + return callback(err) + } + return createInvite(owner, projectId, email, function(err, invite) { + if (err) { + return callback(err) + } + const link = CollaboratorsEmailHandler._buildInviteUrl(project, invite) + return callback(null, project, invite, link) + }) + }) +} + +const revokeInvite = function(sendingUser, projectId, inviteId, callback) { + if (callback == null) { + callback = function(err) {} + } + return sendingUser.getCsrfToken(function(err) { + if (err) { + return callback(err) + } + return sendingUser.request.delete( + { + uri: `/project/${projectId}/invite/${inviteId}` + }, + function(err, response, body) { + if (err) { + return callback(err) + } + return callback(null) + } + ) + }) +} + +// Actions +const tryFollowInviteLink = function(user, link, callback) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.request.get( + { + uri: link, + baseUrl: null + }, + callback + ) +} + +const tryAcceptInvite = function(user, invite, callback) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.request.post( + { + uri: `/project/${invite.projectId}/invite/token/${invite.token}/accept`, + json: { + token: invite.token + } + }, + callback + ) +} + +const tryRegisterUser = function(user, email, callback) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return user.request.post( + { + url: '/register', + json: { + email, + password: 'some_weird_password' + } + }, + callback + ) + }) +} + +const tryFollowLoginLink = function(user, loginLink, callback) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return user.request.get(loginLink, callback) + }) +} + +const tryLoginUser = function(user, callback) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return user.request.post( + { + url: '/login', + json: { + email: user.email, + password: user.password + } + }, + callback + ) + }) +} + +const tryGetInviteList = function(user, projectId, callback) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return user.request.get( + { + url: `/project/${projectId}/invites`, + json: true + }, + callback + ) + }) +} + +const tryJoinProject = function(user, projectId, callback) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return user.request.post( + { + url: `/project/${projectId}/join`, + qs: { user_id: user._id }, + auth: { + user: settings.apis.web.user, + pass: settings.apis.web.pass, + sendImmediately: true + }, + json: true, + jar: false + }, + callback + ) + }) +} + +// Expectations +const expectProjectAccess = function(user, projectId, callback) { + // should have access to project + if (callback == null) { + callback = function(err, result) {} + } + return user.openProject(projectId, err => { + expect(err).to.be.oneOf([null, undefined]) + return callback() + }) +} + +const expectNoProjectAccess = function(user, projectId, callback) { + // should not have access to project page + if (callback == null) { + callback = function(err, result) {} + } + return user.openProject(projectId, err => { + expect(err).to.be.instanceof(Error) + return callback() + }) +} + +const expectInvitePage = function(user, link, callback) { + // view invite + if (callback == null) { + callback = function(err, result) {} + } + return tryFollowInviteLink(user, link, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + expect(body).to.match(new RegExp('Project Invite - .*')) + return callback() + }) +} + +const expectInvalidInvitePage = function(user, link, callback) { + // view invalid invite + if (callback == null) { + callback = function(err, result) {} + } + return tryFollowInviteLink(user, link, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + expect(body).to.match(new RegExp('Invalid Invite - .*')) + return callback() + }) +} + +const expectInviteRedirectToRegister = function(user, link, callback) { + // view invite, redirect to `/register` + if (callback == null) { + callback = function(err, result) {} + } + return tryFollowInviteLink(user, link, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(302) + expect(response.headers.location).to.match(new RegExp('^/register.*$')) + // follow redirect to register page and extract the redirectUrl from form + return user.request.get(response.headers.location, (err, response, body) => + callback(null) + ) + }) +} + +const expectLoginPage = function(user, callback) { + if (callback == null) { + callback = function(err, result) {} + } + return tryFollowLoginLink(user, '/login', function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + expect(body).to.match(new RegExp('Login - .*')) + return callback(null) + }) +} + +const expectLoginRedirectToInvite = function(user, link, callback) { + if (callback == null) { + callback = function(err, result) {} + } + return tryLoginUser(user, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + return callback(null, null) + }) +} + +const expectRegistrationRedirectToInvite = function( + user, + email, + link, + callback +) { + if (callback == null) { + callback = function(err, result) {} + } + return tryRegisterUser(user, email, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + return callback(null, null) + }) +} + +const expectInviteRedirectToProject = function(user, link, invite, callback) { + // view invite, redirect straight to project + if (callback == null) { + callback = function(err, result) {} + } + return tryFollowInviteLink(user, link, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(302) + expect(response.headers.location).to.equal(`/project/${invite.projectId}`) + return callback() + }) +} + +const expectAcceptInviteAndRedirect = function(user, invite, callback) { + // should accept the invite and redirect to project + if (callback == null) { + callback = function(err, result) {} + } + return tryAcceptInvite(user, invite, (err, response, body) => { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(302) + expect(response.headers.location).to.equal(`/project/${invite.projectId}`) + return callback() + }) +} + +const expectInviteListCount = function(user, projectId, count, callback) { + if (callback == null) { + callback = function(err) {} + } + return tryGetInviteList(user, projectId, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + expect(body).to.have.all.keys(['invites']) + expect(body.invites.length).to.equal(count) + return callback() + }) +} + +const expectInvitesInJoinProjectCount = function( + user, + projectId, + count, + callback +) { + if (callback == null) { + callback = function(err, result) {} + } + return tryJoinProject(user, projectId, function(err, response, body) { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + expect(body.project).to.contain.keys(['invites']) + expect(body.project.invites.length).to.equal(count) + return callback() + }) +} + +describe('ProjectInviteTests', function() { + before(function(done) { + this.sendingUser = new User() + this.user = new User() + this.site_admin = new User({ email: 'admin@example.com' }) + this.email = 'smoketestuser@example.com' + this.projectName = 'sharing test' + return Async.series( + [ + cb => this.user.ensureUserExists(cb), + cb => this.sendingUser.login(cb), + cb => this.sendingUser.setFeatures({ collaborators: 10 }, cb) + ], + done + ) + }) + + describe('creating invites', function() { + beforeEach(function(done) { + this.projectName = 'wat' + this.projectId = null + this.fakeProject = null + return done() + }) + + return describe('creating two invites', function() { + beforeEach(function(done) { + return Async.series( + [ + cb => { + return createProject( + this.sendingUser, + this.projectName, + (err, projectId, project) => { + this.projectId = projectId + this.fakeProject = project + return cb() + } + ) + } + ], + done + ) + }) + + afterEach(function(done) { + return Async.series( + [ + cb => this.sendingUser.deleteProject(this.projectId, cb), + cb => this.sendingUser.deleteProject(this.projectId, cb) + ], + done + ) + }) + + it('should allow the project owner to create and remove invites', function(done) { + this.invite = null + return Async.series( + [ + cb => expectProjectAccess(this.sendingUser, this.projectId, cb), + cb => + expectInviteListCount(this.sendingUser, this.projectId, 0, cb), + // create invite, check invite list count + cb => { + return createInvite( + this.sendingUser, + this.projectId, + this.email, + (err, invite) => { + if (err) { + return cb(err) + } + this.invite = invite + return cb() + } + ) + }, + cb => + expectInviteListCount(this.sendingUser, this.projectId, 1, cb), + cb => + revokeInvite( + this.sendingUser, + this.projectId, + this.invite._id, + cb + ), + cb => + expectInviteListCount(this.sendingUser, this.projectId, 0, cb), + // and a second time + cb => { + return createInvite( + this.sendingUser, + this.projectId, + this.email, + (err, invite) => { + if (err) { + return cb(err) + } + this.invite = invite + return cb() + } + ) + }, + cb => + expectInviteListCount(this.sendingUser, this.projectId, 1, cb), + // check the joinProject view + cb => + expectInvitesInJoinProjectCount( + this.sendingUser, + this.projectId, + 1, + cb + ), + // revoke invite + cb => + revokeInvite( + this.sendingUser, + this.projectId, + this.invite._id, + cb + ), + cb => + expectInviteListCount(this.sendingUser, this.projectId, 0, cb), + cb => + expectInvitesInJoinProjectCount( + this.sendingUser, + this.projectId, + 0, + cb + ) + ], + done + ) + }) + + return it('should allow the project owner to create many invites at once', function(done) { + this.inviteOne = null + this.inviteTwo = null + return Async.series( + [ + cb => expectProjectAccess(this.sendingUser, this.projectId, cb), + cb => + expectInviteListCount(this.sendingUser, this.projectId, 0, cb), + // create first invite + cb => { + return createInvite( + this.sendingUser, + this.projectId, + this.email, + (err, invite) => { + if (err) { + return cb(err) + } + this.inviteOne = invite + return cb() + } + ) + }, + cb => + expectInviteListCount(this.sendingUser, this.projectId, 1, cb), + // and a second + cb => { + return createInvite( + this.sendingUser, + this.projectId, + this.email, + (err, invite) => { + if (err) { + return cb(err) + } + this.inviteTwo = invite + return cb() + } + ) + }, + // should have two + cb => + expectInviteListCount(this.sendingUser, this.projectId, 2, cb), + cb => + expectInvitesInJoinProjectCount( + this.sendingUser, + this.projectId, + 2, + cb + ), + // revoke first + cb => + revokeInvite( + this.sendingUser, + this.projectId, + this.inviteOne._id, + cb + ), + cb => + expectInviteListCount(this.sendingUser, this.projectId, 1, cb), + // revoke second + cb => + revokeInvite( + this.sendingUser, + this.projectId, + this.inviteTwo._id, + cb + ), + cb => expectInviteListCount(this.sendingUser, this.projectId, 0, cb) + ], + done + ) + }) + }) + }) + + return describe('clicking the invite link', function() { + beforeEach(function(done) { + this.projectId = null + this.fakeProject = null + return done() + }) + + describe('user is logged in already', function() { + beforeEach(function(done) { + return Async.series( + [ + cb => { + return createProjectAndInvite( + this.sendingUser, + this.projectName, + this.email, + (err, project, invite, link) => { + this.projectId = project._id + this.fakeProject = project + this.invite = invite + this.link = link + return cb() + } + ) + }, + cb => { + return this.user.login(err => { + if (err) { + throw err + } + return cb() + }) + } + ], + done + ) + }) + + afterEach(function(done) { + return Async.series( + [ + cb => this.sendingUser.deleteProject(this.projectId, cb), + cb => this.sendingUser.deleteProject(this.projectId, cb), + cb => + revokeInvite( + this.sendingUser, + this.projectId, + this.invite._id, + cb + ) + ], + done + ) + }) + + describe('user is already a member of the project', function() { + beforeEach(function(done) { + return Async.series( + [ + cb => expectInvitePage(this.user, this.link, cb), + cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb), + cb => expectProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + return describe('when user clicks on the invite a second time', function() { + it('should just redirect to the project page', function(done) { + return Async.series( + [ + cb => expectProjectAccess(this.user, this.invite.projectId, cb), + cb => + expectInviteRedirectToProject( + this.user, + this.link, + this.invite, + cb + ), + cb => expectProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + return describe('when the user recieves another invite to the same project', () => + it('should redirect to the project page', function(done) { + return Async.series( + [ + cb => { + return createInvite( + this.sendingUser, + this.projectId, + this.email, + (err, invite) => { + if (err) { + throw err + } + this.secondInvite = invite + this.secondLink = CollaboratorsEmailHandler._buildInviteUrl( + this.fakeProject, + invite + ) + return cb() + } + ) + }, + cb => + expectInviteRedirectToProject( + this.user, + this.secondLink, + this.secondInvite, + cb + ), + cb => + expectProjectAccess(this.user, this.invite.projectId, cb), + cb => + revokeInvite( + this.sendingUser, + this.projectId, + this.secondInvite._id, + cb + ) + ], + done + ) + })) + }) + }) + + return describe('user is not a member of the project', function() { + it('should not grant access if the user does not accept the invite', function(done) { + return Async.series( + [ + cb => expectInvitePage(this.user, this.link, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + it('should render the invalid-invite page if the token is invalid', function(done) { + return Async.series( + [ + cb => { + const link = this.link.replace( + this.invite.token, + 'not_a_real_token' + ) + return expectInvalidInvitePage(this.user, link, cb) + }, + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + return it('should allow the user to accept the invite and access the project', function(done) { + return Async.series( + [ + cb => expectInvitePage(this.user, this.link, cb), + cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb), + cb => expectProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + }) + }) + + return describe('user is not logged in initially', function() { + before(function(done) { + return this.user.logout(done) + }) + + beforeEach(function(done) { + return Async.series( + [ + cb => { + return createProjectAndInvite( + this.sendingUser, + this.projectName, + this.email, + (err, project, invite, link) => { + this.projectId = project._id + this.fakeProject = project + this.invite = invite + this.link = link + return cb() + } + ) + } + ], + done + ) + }) + + afterEach(function(done) { + return Async.series( + [ + cb => this.sendingUser.deleteProject(this.projectId, cb), + cb => this.sendingUser.deleteProject(this.projectId, cb), + cb => + revokeInvite( + this.sendingUser, + this.projectId, + this.invite._id, + cb + ) + ], + done + ) + }) + + describe('registration prompt workflow with valid token', function() { + it('should redirect to the register page', function(done) { + return Async.series( + [cb => expectInviteRedirectToRegister(this.user, this.link, cb)], + done + ) + }) + + return it('should allow user to accept the invite if the user registers a new account', function(done) { + return Async.series( + [ + cb => expectInviteRedirectToRegister(this.user, this.link, cb), + cb => + expectRegistrationRedirectToInvite( + this.user, + 'some_email@example.com', + this.link, + cb + ), + cb => expectInvitePage(this.user, this.link, cb), + cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb), + cb => expectProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + }) + + describe('registration prompt workflow with non-valid token', function() { + before(function(done) { + return this.user.logout(done) + }) + + it('should redirect to the register page', function(done) { + return Async.series( + [ + cb => expectInviteRedirectToRegister(this.user, this.link, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + return it('should display invalid-invite if the user registers a new account', function(done) { + const badLink = this.link.replace( + this.invite.token, + 'not_a_real_token' + ) + return Async.series( + [ + cb => expectInviteRedirectToRegister(this.user, badLink, cb), + cb => + expectRegistrationRedirectToInvite( + this.user, + 'some_email@example.com', + badLink, + cb + ), + cb => expectInvalidInvitePage(this.user, badLink, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + }) + + describe('login workflow with valid token', function() { + before(function(done) { + return this.user.logout(done) + }) + + it('should redirect to the register page', function(done) { + return Async.series( + [ + cb => expectInviteRedirectToRegister(this.user, this.link, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + it('should allow the user to login to view the invite', function(done) { + return Async.series( + [ + cb => expectInviteRedirectToRegister(this.user, this.link, cb), + cb => expectLoginPage(this.user, cb), + cb => expectLoginRedirectToInvite(this.user, this.link, cb), + cb => expectInvitePage(this.user, this.link, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + return it('should allow user to accept the invite if the user registers a new account', function(done) { + return Async.series( + [ + cb => expectInvitePage(this.user, this.link, cb), + cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb), + cb => expectProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + }) + + return describe('login workflow with non-valid token', function() { + before(function(done) { + return this.user.logout(done) + }) + + it('should redirect to the register page', function(done) { + return Async.series( + [ + cb => expectInviteRedirectToRegister(this.user, this.link, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + + return it('should show the invalid-invite page once the user has logged in', function(done) { + const badLink = this.link.replace( + this.invite.token, + 'not_a_real_token' + ) + return Async.series( + [ + cb => { + return expectInviteRedirectToRegister(this.user, badLink, cb) + }, + cb => { + return expectLoginPage(this.user, cb) + }, + cb => expectLoginRedirectToInvite(this.user, badLink, cb), + cb => expectInvalidInvitePage(this.user, badLink, cb), + cb => expectNoProjectAccess(this.user, this.invite.projectId, cb) + ], + done + ) + }) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/ProjectStructureMongoLockTest.js b/services/web/test/acceptance/src/ProjectStructureMongoLockTest.js new file mode 100644 index 0000000000..cbe1b3c204 --- /dev/null +++ b/services/web/test/acceptance/src/ProjectStructureMongoLockTest.js @@ -0,0 +1,165 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const APP_PATH = '../../../app/src' + +const LockManager = require(`${APP_PATH}/infrastructure/LockManager`) +const ProjectCreationHandler = require(`${APP_PATH}/Features/Project/ProjectCreationHandler.js`) +const ProjectGetter = require(`${APP_PATH}/Features/Project/ProjectGetter.js`) +const ProjectEntityMongoUpdateHandler = require(`${APP_PATH}/Features/Project/ProjectEntityMongoUpdateHandler.js`) +const UserCreator = require(`${APP_PATH}/Features/User/UserCreator.js`) + +const { expect } = require('chai') +const _ = require('lodash') + +// These tests are neither acceptance tests nor unit tests. It's difficult to +// test/verify that our locking is doing what we hope. +// These tests call methods in ProjectGetter and ProjectEntityMongoUpdateHandler +// to see that they DO NOT work when a lock has been taken. +// +// It is tested that these methods DO work when the lock has not been taken in +// other acceptance tests. + +describe('ProjectStructureMongoLock', () => + describe('whilst a project lock is taken', function() { + before(function(done) { + // We want to instantly fail if the lock is taken + LockManager.MAX_LOCK_WAIT_TIME = 1 + this.lockValue = 'lock-value' + const userDetails = { + holdingAccount: false, + email: 'test@example.com' + } + UserCreator.createNewUser(userDetails, (err, user) => { + this.user = user + if (err != null) { + throw err + } + return ProjectCreationHandler.createBlankProject( + user._id, + 'locked-project', + (err, project) => { + if (err != null) { + throw err + } + this.locked_project = project + const namespace = ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE + this.lock_key = `lock:web:${namespace}:${project._id}` + return LockManager._getLock( + this.lock_key, + namespace, + (err, lockValue) => { + this.lockValue = lockValue + return done() + } + ) + } + ) + }) + }) + + after(function(done) { + return LockManager._releaseLock(this.lock_key, this.lockValue, done) + }) + + describe('interacting with the locked project', function() { + const LOCKING_UPDATE_METHODS = [ + 'addDoc', + 'addFile', + 'mkdirp', + 'moveEntity', + 'renameEntity', + 'addFolder' + ] + for (var methodName of Array.from(LOCKING_UPDATE_METHODS)) { + it(`cannot call ProjectEntityMongoUpdateHandler.${methodName}`, function(done) { + const method = ProjectEntityMongoUpdateHandler[methodName] + const args = _.times(method.length - 2, _.constant(null)) + return method(this.locked_project._id, args, function(err) { + expect(err).to.deep.equal(new Error('Timeout')) + return done() + }) + }) + } + + it('cannot get the project without a projection', function(done) { + return ProjectGetter.getProject(this.locked_project._id, function(err) { + expect(err).to.deep.equal(new Error('Timeout')) + return done() + }) + }) + + it('cannot get the project if rootFolder is in the projection', function(done) { + return ProjectGetter.getProject( + this.locked_project._id, + { rootFolder: true }, + function(err) { + expect(err).to.deep.equal(new Error('Timeout')) + return done() + } + ) + }) + + return it('can get the project if rootFolder is not in the projection', function(done) { + return ProjectGetter.getProject( + this.locked_project._id, + { _id: true }, + (err, project) => { + expect(err).to.equal(null) + expect(project._id).to.deep.equal(this.locked_project._id) + return done() + } + ) + }) + }) + + return describe('interacting with other projects', function() { + before(function(done) { + return ProjectCreationHandler.createBlankProject( + this.user._id, + 'unlocked-project', + (err, project) => { + if (err != null) { + throw err + } + this.unlocked_project = project + return done() + } + ) + }) + + it('can add folders to other projects', function(done) { + return ProjectEntityMongoUpdateHandler.addFolder( + this.unlocked_project._id, + this.unlocked_project.rootFolder[0]._id, + 'new folder', + function(err, folder) { + expect(err).to.equal(null) + expect(folder).to.be.defined + return done() + } + ) + }) + + return it('can get other projects without a projection', function(done) { + return ProjectGetter.getProject( + this.unlocked_project._id, + (err, project) => { + expect(err).to.equal(null) + expect(project._id).to.deep.equal(this.unlocked_project._id) + return done() + } + ) + }) + }) + })) diff --git a/services/web/test/acceptance/src/ProjectStructureTests.js b/services/web/test/acceptance/src/ProjectStructureTests.js new file mode 100644 index 0000000000..4e122b69d1 --- /dev/null +++ b/services/web/test/acceptance/src/ProjectStructureTests.js @@ -0,0 +1,1445 @@ +/* eslint-disable + camelcase, + max-len, + no-path-concat, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async') +const { expect } = require('chai') +const mkdirp = require('mkdirp') +const { ObjectId } = require('mongojs') +const Path = require('path') +const fs = require('fs') +const Settings = require('settings-sharelatex') +const _ = require('underscore') + +const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js') + +const MockDocUpdaterApi = require('./helpers/MockDocUpdaterApi') +const MockFileStoreApi = require('./helpers/MockFileStoreApi') +const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi') +const request = require('./helpers/request') +const User = require('./helpers/User') + +describe('ProjectStructureChanges', function() { + let example_project_id = null + let example_doc_id = null + let example_file_id = null + let example_folder_id_1 = null + let example_folder_id_2 = null + + before(function(done) { + this.owner = new User() + return this.owner.login(done) + }) + + describe('creating a project from the example template', function() { + before(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return this.owner.createProject( + 'example-project', + { template: 'example' }, + (error, project_id) => { + if (error != null) { + throw error + } + example_project_id = project_id + return done() + } + ) + }) + + it('should version creating a doc', function() { + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(2) + _.each(updates, update => { + expect(update.userId).to.equal(this.owner._id) + return expect(update.docLines).to.be.a('string') + }) + expect(_.where(updates, { pathname: '/main.tex' }).length).to.equal(1) + expect(_.where(updates, { pathname: '/references.bib' }).length).to.equal( + 1 + ) + return expect(version).to.equal(3) + }) + + return it('should version creating a file', function() { + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/universe.jpg') + expect(update.url).to.be.a('string') + return expect(version).to.equal(3) + }) + }) + + describe('duplicating a project', function() { + before(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return this.owner.request.post( + { + uri: `/Project/${example_project_id}/clone`, + json: { + projectName: 'new.tex' + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to add doc ${res.statusCode}`) + } + this.dup_project_id = body.project_id + return done() + } + ) + }) + + it('should version the docs created', function() { + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.dup_project_id) + expect(updates.length).to.equal(2) + _.each(updates, update => { + expect(update.userId).to.equal(this.owner._id) + return expect(update.docLines).to.be.a('string') + }) + expect(_.where(updates, { pathname: '/main.tex' }).length).to.equal(1) + expect(_.where(updates, { pathname: '/references.bib' }).length).to.equal( + 1 + ) + return expect(version).to.equal(3) + }) + + return it('should version the files created', function() { + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.dup_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/universe.jpg') + expect(update.url).to.be.a('string') + return expect(version).to.equal(3) + }) + }) + + describe('adding a doc', function() { + before(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + + return ProjectGetter.getProject(example_project_id, (error, project) => { + if (error != null) { + throw error + } + this.project_0 = project + return this.owner.request.post( + { + uri: `project/${example_project_id}/doc`, + json: { + name: 'new.tex', + parent_folder_id: project.rootFolder[0]._id + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to add doc ${res.statusCode}`) + } + example_doc_id = body._id + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + return done() + } + ) + } + ) + }) + }) + + it('should version the doc added', function() { + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/new.tex') + expect(update.docLines).to.be.a('string') + return expect(version).to.equal(this.project_0.version + 1) + }) + + return it('should increment the project structure version number', function() { + return expect(this.project_1.version).to.equal(this.project_0.version + 1) + }) + }) + + describe('uploading a project', function() { + before(function(done) { + let req + MockDocUpdaterApi.clearProjectStructureUpdates() + + const zip_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/test_project.zip') + ) + this.test_project_name = 'wombat' + + return (req = this.owner.request.post( + { + uri: 'project/new/upload', + formData: { + qqfile: zip_file + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload project ${res.statusCode}`) + } + this.uploaded_project_id = JSON.parse(body).project_id + return done() + } + )) + }) + + it('should version the docs created', function() { + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.uploaded_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/main.tex') + expect(update.docLines).to.equal('Test') + return expect(version).to.equal(2) + }) + + return it('should version the files created', function() { + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.uploaded_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + expect(update.url).to.be.a('string') + return expect(version).to.equal(2) + }) + }) + + describe('uploading a project with a name', function() { + before(function(done) { + let req + MockDocUpdaterApi.clearProjectStructureUpdates() + + const zip_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/test_project_with_name.zip') + ) + this.test_project_name = 'wombat' + + return (req = this.owner.request.post( + { + uri: 'project/new/upload', + formData: { + qqfile: zip_file + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload project ${res.statusCode}`) + } + this.uploaded_project_id = JSON.parse(body).project_id + return done() + } + )) + }) + + return it('should set the project name from the zip contents', function(done) { + return ProjectGetter.getProject( + this.uploaded_project_id, + (error, project) => { + expect(error).not.to.exist + expect(project.name).to.equal(this.test_project_name) + return done() + } + ) + }) + }) + + describe('uploading a project with an invalid name', function() { + before(function(done) { + let req + MockDocUpdaterApi.clearProjectStructureUpdates() + + const zip_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/test_project_with_invalid_name.zip') + ) + this.test_project_match = /^bad[^\\]+name$/ + + return (req = this.owner.request.post( + { + uri: 'project/new/upload', + formData: { + qqfile: zip_file + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload project ${res.statusCode}`) + } + this.uploaded_project_id = JSON.parse(body).project_id + return done() + } + )) + }) + + return it('should set the project name from the zip contents', function(done) { + return ProjectGetter.getProject( + this.uploaded_project_id, + (error, project) => { + expect(error).not.to.exist + expect(project.name).to.match(this.test_project_match) + return done() + } + ) + }) + }) + + describe('uploading a project with a shared top-level folder', function() { + before(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + + const zip_file = fs.createReadStream( + Path.resolve( + __dirname + '/../files/test_project_with_shared_top_level_folder.zip' + ) + ) + + return this.owner.request.post( + { + uri: 'project/new/upload', + formData: { + qqfile: zip_file + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload project ${res.statusCode}`) + } + this.uploaded_project_id = JSON.parse(body).project_id + return done() + } + ) + }) + + return it('should not create the top-level folder', function(done) { + return ProjectGetter.getProject(this.uploaded_project_id, function( + error, + project + ) { + expect(error).not.to.exist + expect(project.rootFolder[0].folders.length).to.equal(0) + expect(project.rootFolder[0].docs.length).to.equal(2) + return done() + }) + }) + }) + + describe('uploading a project with backslashes in the path names', function() { + before(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + + const zip_file = fs.createReadStream( + Path.resolve( + __dirname + '/../files/test_project_with_backslash_in_filename.zip' + ) + ) + + return this.owner.request.post( + { + uri: 'project/new/upload', + formData: { + qqfile: zip_file + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload project ${res.statusCode}`) + } + this.uploaded_project_id = JSON.parse(body).project_id + return done() + } + ) + }) + + return it('should treat the backslash as a directory separator', function(done) { + return ProjectGetter.getProject(this.uploaded_project_id, function( + error, + project + ) { + expect(error).not.to.exist + expect(project.rootFolder[0].folders[0].name).to.equal('styles') + expect(project.rootFolder[0].folders[0].docs[0].name).to.equal('ao.sty') + return done() + }) + }) + }) + + describe('uploading a project with files in different encodings', function() { + before(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + + const zip_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/charsets/charsets.zip') + ) + + return this.owner.request.post( + { + uri: 'project/new/upload', + formData: { + qqfile: zip_file + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload project ${res.statusCode}`) + } + this.uploaded_project_id = JSON.parse(body).project_id + return done() + } + ) + }) + + it('should correctly parse windows-1252', function() { + const { + docUpdates: updates + } = MockDocUpdaterApi.getProjectStructureUpdates(this.uploaded_project_id) + const update = _.find( + updates, + update => update.pathname === '/test-german-windows-1252.tex' + ) + return expect(update.docLines).to.contain( + 'Der schnelle braune Fuchs sprang träge über den Hund.' + ) + }) + + it('should correctly parse German utf8', function() { + const { + docUpdates: updates + } = MockDocUpdaterApi.getProjectStructureUpdates(this.uploaded_project_id) + const update = _.find( + updates, + update => update.pathname === '/test-german-utf8x.tex' + ) + return expect(update.docLines).to.contain( + 'Der schnelle braune Fuchs sprang träge über den Hund.' + ) + }) + + it('should correctly parse little-endian utf16', function() { + const { + docUpdates: updates + } = MockDocUpdaterApi.getProjectStructureUpdates(this.uploaded_project_id) + const update = _.find( + updates, + update => update.pathname === '/test-greek-utf16-le-bom.tex' + ) + return expect(update.docLines).to.contain( + 'Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.' + ) + }) + + return it('should correctly parse Greek utf8', function() { + const { + docUpdates: updates + } = MockDocUpdaterApi.getProjectStructureUpdates(this.uploaded_project_id) + const update = _.find( + updates, + update => update.pathname === '/test-greek-utf8x.tex' + ) + return expect(update.docLines).to.contain( + 'Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.' + ) + }) + }) + + describe('uploading a file', function() { + beforeEach(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return ProjectGetter.getProject(example_project_id, (error, project) => { + if (error != null) { + throw error + } + this.root_folder_id = project.rootFolder[0]._id.toString() + this.project_0 = project + return done() + }) + }) + + it('should version a newly uploaded file', function(done) { + let req + const image_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/1pixel.png') + ) + + return (req = this.owner.request.post( + { + uri: `project/${example_project_id}/upload`, + qs: { + folder_id: this.root_folder_id + }, + formData: { + qqfile: { + value: image_file, + options: { + filename: '1pixel.png', + contentType: 'image/png' + } + } + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload file ${res.statusCode}`) + } + + example_file_id = JSON.parse(body).entity_id + + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + expect(update.url).to.be.a('string') + this.original_file_url = update.url + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // uploading a new file does change the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 1 + ) + return done() + } + ) + } + )) + }) + + return it('should version a replacement file', function(done) { + let req + const image_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/2pixel.png') + ) + + return (req = this.owner.request.post( + { + uri: `project/${example_project_id}/upload`, + qs: { + folder_id: this.root_folder_id + }, + formData: { + qqfile: { + value: image_file, + options: { + filename: '1pixel.png', + contentType: 'image/png' + } + } + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload file ${res.statusCode}`) + } + + example_file_id = JSON.parse(body).entity_id + + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(2) + let update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + // expect(update.url).to.be.a('string'); + update = updates[1] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + expect(update.url).to.be.a('string') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 1 + ) + return done() + } + ) + } + )) + }) + }) + + describe('moving entities', function() { + before(function(done) { + return this.owner.request.post( + { + uri: `project/${example_project_id}/folder`, + json: { + name: 'foo' + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + example_folder_id_1 = body._id + return done() + } + ) + }) + + beforeEach(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return ProjectGetter.getProject(example_project_id, (error, project) => { + if (error != null) { + throw error + } + this.root_folder_id = project.rootFolder[0]._id.toString() + this.project_0 = project + return done() + }) + }) + + it('should version moving a doc', function(done) { + return this.owner.request.post( + { + uri: `project/${example_project_id}/Doc/${example_doc_id}/move`, + json: { + folder_id: example_folder_id_1 + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to move doc ${res.statusCode}`) + } + + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/new.tex') + expect(update.newPathname).to.equal('/foo/new.tex') + expect(version).to.equal(this.project_0.version + 2) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 2 + ) // 2 because it's a delete and then add + return done() + } + ) + } + ) + }) + + it('should version moving a file', function(done) { + return this.owner.request.post( + { + uri: `project/${example_project_id}/File/${example_file_id}/move`, + json: { + folder_id: example_folder_id_1 + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to move file ${res.statusCode}`) + } + + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + expect(update.newPathname).to.equal('/foo/1pixel.png') + expect(version).to.equal(this.project_0.version + 2) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 2 + ) // 2 because it's a delete and then add + return done() + } + ) + } + ) + }) + + return it('should version moving a folder', function(done) { + return this.owner.request.post( + { + uri: `project/${example_project_id}/folder`, + json: { + name: 'bar' + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + example_folder_id_2 = body._id + + return this.owner.request.post( + { + uri: `project/${example_project_id}/Folder/${example_folder_id_1}/move`, + json: { + folder_id: example_folder_id_2 + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to move folder ${res.statusCode}`) + } + + let { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates( + example_project_id + ) + expect(updates.length).to.equal(1) + let update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/foo/new.tex') + expect(update.newPathname).to.equal('/bar/foo/new.tex') + expect(version).to.equal(this.project_0.version + 3) + ;({ + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates( + example_project_id + )) + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/foo/1pixel.png') + expect(update.newPathname).to.equal('/bar/foo/1pixel.png') + expect(version).to.equal(this.project_0.version + 3) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 3 + ) // because folder and 2 files move + return done() + } + ) + } + ) + } + ) + }) + }) + + describe('renaming entities', function() { + beforeEach(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return ProjectGetter.getProject(example_project_id, (error, project) => { + if (error != null) { + throw error + } + this.root_folder_id = project.rootFolder[0]._id.toString() + this.project_0 = project + return done() + }) + }) + + it('should version renaming a doc', function(done) { + return this.owner.request.post( + { + uri: `project/${example_project_id}/Doc/${example_doc_id}/rename`, + json: { + name: 'new_renamed.tex' + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to move doc ${res.statusCode}`) + } + + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/bar/foo/new.tex') + expect(update.newPathname).to.equal('/bar/foo/new_renamed.tex') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 1 + ) + return done() + } + ) + } + ) + }) + + it('should version renaming a file', function(done) { + return this.owner.request.post( + { + uri: `project/${example_project_id}/File/${example_file_id}/rename`, + json: { + name: '1pixel_renamed.png' + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to move file ${res.statusCode}`) + } + + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/bar/foo/1pixel.png') + expect(update.newPathname).to.equal('/bar/foo/1pixel_renamed.png') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 1 + ) + return done() + } + ) + } + ) + }) + + return it('should version renaming a folder', function(done) { + return this.owner.request.post( + { + uri: `project/${example_project_id}/Folder/${example_folder_id_1}/rename`, + json: { + name: 'foo_renamed' + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to move folder ${res.statusCode}`) + } + + let { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + let update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/bar/foo/new_renamed.tex') + expect(update.newPathname).to.equal( + '/bar/foo_renamed/new_renamed.tex' + ) + expect(version).to.equal(this.project_0.version + 1) + ;({ + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)) + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/bar/foo/1pixel_renamed.png') + expect(update.newPathname).to.equal( + '/bar/foo_renamed/1pixel_renamed.png' + ) + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 1 + ) + return done() + } + ) + } + ) + }) + }) + + describe('deleting entities', function() { + beforeEach(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return ProjectGetter.getProject(example_project_id, (error, project) => { + if (error != null) { + throw error + } + this.root_folder_id = project.rootFolder[0]._id.toString() + this.project_0 = project + return done() + }) + }) + + return it('should version deleting a folder', function(done) { + return this.owner.request.delete( + { + uri: `project/${example_project_id}/Folder/${example_folder_id_2}` + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to delete folder ${res.statusCode}`) + } + + let { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + expect(updates.length).to.equal(1) + let update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/bar/foo_renamed/new_renamed.tex') + expect(update.newPathname).to.equal('') + expect(version).to.equal(this.project_0.version + 1) + ;({ + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id)) + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal( + '/bar/foo_renamed/1pixel_renamed.png' + ) + expect(update.newPathname).to.equal('') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + example_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 1 + ) + return done() + } + ) + } + ) + }) + }) + + describe('tpds', function() { + before(function(done) { + this.tpds_project_name = `tpds-project-${new ObjectId().toString()}` + return this.owner.createProject( + this.tpds_project_name, + (error, project_id) => { + if (error != null) { + throw error + } + this.tpds_project_id = project_id + return mkdirp(Settings.path.dumpFolder, done) + } + ) + }) + + beforeEach(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return ProjectGetter.getProject( + this.tpds_project_id, + (error, project) => { + if (error != null) { + throw error + } + this.root_folder_id = project.rootFolder[0]._id.toString() + this.project_0 = project + return done() + } + ) + }) + + it('should version adding a doc', function(done) { + const tex_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/test.tex') + ) + + const req = this.owner.request.post({ + uri: `/user/${this.owner._id}/update/${ + this.tpds_project_name + }/test.tex`, + auth: { + user: _.keys(Settings.httpAuthUsers)[0], + pass: _.values(Settings.httpAuthUsers)[0], + sendImmediately: true + } + }) + + tex_file.on('error', function(err) { + throw err + }) + + req.on('error', function(err) { + throw err + }) + + req.on('response', res => { + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload file ${res.statusCode}`) + } + + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.tpds_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/test.tex') + expect(update.docLines).to.equal('Test') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + this.tpds_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal(this.project_0.version + 1) + return done() + } + ) + }) + + return tex_file.pipe(req) + }) + + it('should version adding a new file', function(done) { + const image_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/1pixel.png') + ) + + const req = this.owner.request.post({ + uri: `/user/${this.owner._id}/update/${ + this.tpds_project_name + }/1pixel.png`, + auth: { + user: _.keys(Settings.httpAuthUsers)[0], + pass: _.values(Settings.httpAuthUsers)[0], + sendImmediately: true + } + }) + + image_file.on('error', function(err) { + throw err + }) + + req.on('error', function(err) { + throw err + }) + + req.on('response', res => { + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload file ${res.statusCode}`) + } + + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.tpds_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + expect(update.url).to.be.a('string') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + this.tpds_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal(this.project_0.version + 1) + return done() + } + ) + }) + + return image_file.pipe(req) + }) + + it('should version replacing a file', function(done) { + const image_file = fs.createReadStream( + Path.resolve(__dirname + '/../files/2pixel.png') + ) + + const req = this.owner.request.post({ + uri: `/user/${this.owner._id}/update/${ + this.tpds_project_name + }/1pixel.png`, + auth: { + user: _.keys(Settings.httpAuthUsers)[0], + pass: _.values(Settings.httpAuthUsers)[0], + sendImmediately: true + } + }) + + image_file.on('error', function(err) { + throw err + }) + + req.on('error', function(err) { + throw err + }) + + req.on('response', res => { + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload file ${res.statusCode}`) + } + + const { + fileUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.tpds_project_id) + expect(updates.length).to.equal(2) + let update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + // expect(update.url).to.be.a('string'); + update = updates[1] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/1pixel.png') + expect(update.url).to.be.a('string') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + this.tpds_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal(this.project_0.version + 1) + return done() + } + ) + }) + + return image_file.pipe(req) + }) + + return it('should version deleting a doc', function(done) { + let req + return (req = this.owner.request.delete( + { + uri: `/user/${this.owner._id}/update/${ + this.tpds_project_name + }/test.tex`, + auth: { + user: _.keys(Settings.httpAuthUsers)[0], + pass: _.values(Settings.httpAuthUsers)[0], + sendImmediately: true + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to delete doc ${res.statusCode}`) + } + + const { + docUpdates: updates, + version + } = MockDocUpdaterApi.getProjectStructureUpdates(this.tpds_project_id) + expect(updates.length).to.equal(1) + const update = updates[0] + expect(update.userId).to.equal(this.owner._id) + expect(update.pathname).to.equal('/test.tex') + expect(update.newPathname).to.equal('') + expect(version).to.equal(this.project_0.version + 1) + + return ProjectGetter.getProject( + this.tpds_project_id, + (error, newProject) => { + if (error != null) { + throw error + } + this.project_1 = newProject + // replacing a file should update the project structure + expect(this.project_1.version).to.equal( + this.project_0.version + 1 + ) + return done() + } + ) + } + )) + }) + }) + + return describe('uploading a document', function() { + beforeEach(function(done) { + MockDocUpdaterApi.clearProjectStructureUpdates() + return ProjectGetter.getProject(example_project_id, (error, project) => { + if (error != null) { + throw error + } + this.root_folder_id = project.rootFolder[0]._id.toString() + this.project_0 = project + return done() + }) + }) + + return describe('with an unusual character set', function() { + it('should correctly handle utf16-le data', function(done) { + let req + const document_file = fs.createReadStream( + Path.resolve( + __dirname + '/../files/charsets/test-greek-utf16-le-bom.tex' + ) + ) + + return (req = this.owner.request.post( + { + uri: `project/${example_project_id}/upload`, + qs: { + folder_id: this.root_folder_id + }, + formData: { + qqfile: { + value: document_file, + options: { + filename: 'test-greek-utf16-le-bom.tex', + contentType: 'text/x-tex' + } + } + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload file ${res.statusCode}`) + } + + example_file_id = JSON.parse(body).entity_id + + const { + docUpdates: updates + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + const update = updates[0] + expect(update.pathname).to.equal('/test-greek-utf16-le-bom.tex') + expect(update.docLines).to.contain( + 'Η γρήγορη καστανή αλεπού πήδηξε χαλαρά πάνω από το σκυλί.' + ) + return done() + } + )) + }) + + return it('should correctly handle windows1252/iso-8859-1/latin1 data', function(done) { + let req + const document_file = fs.createReadStream( + Path.resolve( + __dirname + '/../files/charsets/test-german-windows-1252.tex' + ) + ) + + return (req = this.owner.request.post( + { + uri: `project/${example_project_id}/upload`, + qs: { + folder_id: this.root_folder_id + }, + formData: { + qqfile: { + value: document_file, + options: { + filename: 'test-german-windows-1252.tex', + contentType: 'text/x-tex' + } + } + } + }, + (error, res, body) => { + if (error != null) { + throw error + } + if (res.statusCode < 200 || res.statusCode >= 300) { + throw new Error(`failed to upload file ${res.statusCode}`) + } + + example_file_id = JSON.parse(body).entity_id + + const { + docUpdates: updates + } = MockDocUpdaterApi.getProjectStructureUpdates(example_project_id) + const update = updates[0] + expect(update.pathname).to.equal('/test-german-windows-1252.tex') + expect(update.docLines).to.contain( + 'Der schnelle braune Fuchs sprang träge über den Hund.' + ) + return done() + } + )) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/ProxyUrls.js b/services/web/test/acceptance/src/ProxyUrls.js new file mode 100644 index 0000000000..5bd6470633 --- /dev/null +++ b/services/web/test/acceptance/src/ProxyUrls.js @@ -0,0 +1,74 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const { assert } = require('chai') +const async = require('async') +const request = require('./helpers/request') +const MockV1Api = require('./helpers/MockV1Api') + +const assertResponse = (path, expectedStatusCode, expectedBody, cb) => + request.get(path, function(error, response) { + should.not.exist(error) + response.statusCode.should.equal(expectedStatusCode) + if (expectedBody) { + assert.deepEqual(JSON.parse(response.body), expectedBody) + } + return cb() + }) + +describe('ProxyUrls', function() { + before(function() { + return this.timeout(1000) + }) + + it('proxy static URLs', done => + async.series( + [ + cb => assertResponse('/institutions/list', 200, [], cb), + cb => assertResponse('/institutions/domains', 200, [], cb) + ], + done + )) + + it('proxy dynamic URLs', done => + async.series( + [ + cb => + assertResponse( + '/institutions/list/123', + 200, + { id: 123, name: 'Institution 123' }, + cb + ), + cb => + assertResponse( + '/institutions/list/456', + 200, + { id: 456, name: 'Institution 456' }, + cb + ) + ], + done + )) + + it('return 404 if proxy is not set', done => + async.series( + [cb => assertResponse('/institutions/foobar', 404, null, cb)], + done + )) + + return it('handle missing baseUrl', done => + async.series( + [cb => assertResponse('/proxy/missing/baseUrl', 500, null, cb)], + done + )) +}) diff --git a/services/web/test/acceptance/src/RedirectUrlsTests.js b/services/web/test/acceptance/src/RedirectUrlsTests.js new file mode 100644 index 0000000000..b28512d218 --- /dev/null +++ b/services/web/test/acceptance/src/RedirectUrlsTests.js @@ -0,0 +1,97 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const { assert } = require('chai') +const async = require('async') +const request = require('./helpers/request') +const MockV1Api = require('./helpers/MockV1Api') + +const assertRedirect = (method, path, expectedStatusCode, destination, cb) => + request[method](path, function(error, response) { + should.not.exist(error) + response.statusCode.should.equal(expectedStatusCode) + response.headers.location.should.equal(destination) + return cb() + }) + +describe('RedirectUrls', function() { + before(function() { + return this.timeout(1000) + }) + + it('proxy static URLs', done => + assertRedirect('get', '/redirect/one', 302, '/destination/one', done)) + + it('proxy dynamic URLs', done => + assertRedirect( + 'get', + '/redirect/params/42', + 302, + '/destination/42/params', + done + )) + + it('proxy URLs with baseUrl', done => + assertRedirect( + 'get', + '/redirect/base_url', + 302, + 'https://example.com/destination/base_url', + done + )) + + it('proxy URLs with POST with a 307', done => + assertRedirect( + 'post', + '/redirect/get_and_post', + 307, + '/destination/get_and_post', + done + )) + + it('proxy URLs with multiple support methods', done => + assertRedirect( + 'get', + '/redirect/get_and_post', + 302, + '/destination/get_and_post', + done + )) + + it('redirects with query params', done => + assertRedirect( + 'get', + '/redirect/qs?foo=bar&baz[]=qux1&baz[]=qux2', + 302, + '/destination/qs?foo=bar&baz[]=qux1&baz[]=qux2', + done + )) + + it("skips redirects if the 'skip-redirects' header is set", done => + request.get( + { url: '/redirect/one', headers: { 'x-skip-redirects': 'true' } }, + function(error, response) { + should.not.exist(error) + response.statusCode.should.equal(404) + return done() + } + )) + + return it('redirects to /sign_in_to_v1 with authWithV1 setting', done => + assertRedirect( + 'get', + '/docs_v1?zip_uri=http%3A%2F%2Foverleaf.test%2Ffoo%3Fbar%3Dbaz%26qux%3Dthing&bar=baz', + 302, + '/sign_in_to_v1?return_to=%2Fdocs%3Fzip_uri%3Dhttp%253A%252F%252Foverleaf.test%252Ffoo%253Fbar%253Dbaz%2526qux%253Dthing%26bar%3Dbaz', + done + )) +}) diff --git a/services/web/test/acceptance/src/RegistrationTests.js b/services/web/test/acceptance/src/RegistrationTests.js new file mode 100644 index 0000000000..016a6e7c46 --- /dev/null +++ b/services/web/test/acceptance/src/RegistrationTests.js @@ -0,0 +1,349 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const { assert } = require('chai') +const async = require('async') +const User = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') +const redis = require('./helpers/redis') +const _ = require('lodash') + +// Currently this is testing registration via the 'public-registration' module, +// whereas in production we're using the 'overleaf-integration' module. + +// Expectations +const expectProjectAccess = function(user, projectId, callback) { + // should have access to project + if (callback == null) { + callback = function(err, result) {} + } + return user.openProject(projectId, err => { + expect(err).to.be.oneOf([null, undefined]) + return callback() + }) +} + +const expectNoProjectAccess = function(user, projectId, callback) { + // should not have access to project page + if (callback == null) { + callback = function(err, result) {} + } + return user.openProject(projectId, err => { + expect(err).to.be.instanceof(Error) + return callback() + }) +} + +// Actions +const tryLoginThroughRegistrationForm = function( + user, + email, + password, + callback +) { + if (callback == null) { + callback = function(err, response, body) {} + } + return user.getCsrfToken(function(err) { + if (err != null) { + return callback(err) + } + return user.request.post( + { + url: '/register', + json: { + email, + password + } + }, + callback + ) + }) +} + +describe('LoginRateLimit', function() { + before(function() { + this.user = new User() + this.badEmail = 'bademail@example.com' + return (this.badPassword = 'badpassword') + }) + + return it('should rate limit login attempts after 10 within two minutes', function(done) { + return this.user.request.get('/login', (err, res, body) => { + return async.timesSeries( + 15, + (n, cb) => { + return this.user.getCsrfToken(error => { + if (error != null) { + return cb(error) + } + return this.user.request.post( + { + url: '/login', + json: { + email: this.badEmail, + password: this.badPassword + } + }, + (err, response, body) => { + return cb( + null, + __guard__( + body != null ? body.message : undefined, + x => x.text + ) + ) + } + ) + }) + }, + (err, results) => { + // ten incorrect-credentials messages, then five rate-limit messages + expect(results.length).to.equal(15) + assert.deepEqual( + results, + _.concat( + _.fill( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + 'Your email or password is incorrect. Please try again' + ), + _.fill( + [1, 2, 3, 4, 5], + 'This account has had too many login requests. Please wait 2 minutes before trying to log in again' + ) + ) + ) + return done() + } + ) + }) + }) +}) + +describe('CSRF protection', function() { + beforeEach(function() { + this.user = new User() + this.email = `test+${Math.random()}@example.com` + return (this.password = 'password11') + }) + + afterEach(function() { + return this.user.full_delete_user(this.email) + }) + + it('should register with the csrf token', function(done) { + return this.user.request.get('/login', (err, res, body) => { + return this.user.getCsrfToken(error => { + return this.user.request.post( + { + url: '/register', + json: { + email: this.email, + password: this.password + }, + headers: { + 'x-csrf-token': this.user.csrfToken + } + }, + (error, response, body) => { + expect(err != null).to.equal(false) + expect(response.statusCode).to.equal(200) + return done() + } + ) + }) + }) + }) + + it('should fail with no csrf token', function(done) { + return this.user.request.get('/login', (err, res, body) => { + return this.user.getCsrfToken(error => { + return this.user.request.post( + { + url: '/register', + json: { + email: this.email, + password: this.password + }, + headers: { + 'x-csrf-token': '' + } + }, + (error, response, body) => { + expect(response.statusCode).to.equal(403) + return done() + } + ) + }) + }) + }) + + return it('should fail with a stale csrf token', function(done) { + return this.user.request.get('/login', (err, res, body) => { + return this.user.getCsrfToken(error => { + const oldCsrfToken = this.user.csrfToken + return this.user.logout(err => { + return this.user.request.post( + { + url: '/register', + json: { + email: this.email, + password: this.password + }, + headers: { + 'x-csrf-token': oldCsrfToken + } + }, + (error, response, body) => { + expect(response.statusCode).to.equal(403) + return done() + } + ) + }) + }) + }) + }) +}) + +describe('Register', function() { + before(function() { + return (this.user = new User()) + }) + + return it('Set emails attribute', function(done) { + return this.user.register((error, user) => { + expect(error).to.not.exist + user.email.should.equal(this.user.email) + user.emails.should.exist + user.emails.should.be.a('array') + user.emails.length.should.equal(1) + user.emails[0].email.should.equal(this.user.email) + return done() + }) + }) +}) + +describe('Register with bonus referal id', function() { + before(function(done) { + this.user1 = new User() + this.user2 = new User() + return async.series( + [ + cb => this.user1.register(cb), + cb => + this.user2.registerWithQuery( + `?r=${this.user1.referal_id}&rm=d&rs=b`, + cb + ) + ], + done + ) + }) + + return it('Adds a referal when an id is supplied and the referal source is "bonus"', function(done) { + return this.user1.get((error, user) => { + expect(error).to.not.exist + user.refered_user_count.should.eql(1) + + return done() + }) + }) +}) + +describe('LoginViaRegistration', function() { + before(function(done) { + this.timeout(60000) + this.user1 = new User() + this.user2 = new User() + async.series( + [ + cb => this.user1.login(cb), + cb => this.user1.logout(cb), + cb => redis.clearUserSessions(this.user1, cb), + cb => this.user2.login(cb), + cb => this.user2.logout(cb), + cb => redis.clearUserSessions(this.user2, cb) + ], + done + ) + return (this.project_id = null) + }) + + return describe('[Security] Trying to register/login as another user', function() { + it('should not allow sign in with secondary email', function(done) { + const secondaryEmail = 'acceptance-test-secondary@example.com' + return this.user1.addEmail(secondaryEmail, err => { + return this.user1.loginWith(secondaryEmail, err => { + expect(err != null).to.equal(false) + return this.user1.isLoggedIn(function(err, isLoggedIn) { + expect(isLoggedIn).to.equal(false) + return done() + }) + }) + }) + }) + + it('should have user1 login', function(done) { + return this.user1.login(function(err) { + expect(err != null).to.equal(false) + return done() + }) + }) + + it('should have user1 create a project', function(done) { + return this.user1.createProject('Private Project', (err, project_id) => { + expect(err != null).to.equal(false) + this.project_id = project_id + return done() + }) + }) + + it('should ensure user1 can access their project', function(done) { + return expectProjectAccess(this.user1, this.project_id, done) + }) + + it('should ensure user2 cannot access the project', function(done) { + return expectNoProjectAccess(this.user2, this.project_id, done) + }) + + it('should prevent user2 from login/register with user1 email address', function(done) { + return tryLoginThroughRegistrationForm( + this.user2, + this.user1.email, + 'totally_not_the_right_password', + (err, response, body) => { + expect(body.redir != null).to.equal(false) + expect(body.message != null).to.equal(true) + expect(body.message).to.have.all.keys('type', 'text') + expect(body.message.type).to.equal('error') + return done() + } + ) + }) + + return it('should still ensure user2 cannot access the project', function(done) { + return expectNoProjectAccess(this.user2, this.project_id, done) + }) + }) +}) + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/test/acceptance/src/RestoringFilesTest.js b/services/web/test/acceptance/src/RestoringFilesTest.js new file mode 100644 index 0000000000..4c2418e7b0 --- /dev/null +++ b/services/web/test/acceptance/src/RestoringFilesTest.js @@ -0,0 +1,350 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async') +const { expect } = require('chai') +const _ = require('underscore') +const fs = require('fs') +const Path = require('path') + +const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js') + +const User = require('./helpers/User') +const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi') +const MockDocstoreApi = require('./helpers/MockDocstoreApi') +const MockFileStoreApi = require('./helpers/MockFileStoreApi') + +describe('RestoringFiles', function() { + before(function(done) { + this.owner = new User() + return this.owner.login(error => { + if (error != null) { + throw error + } + return this.owner.createProject( + 'example-project', + { template: 'example' }, + (error, project_id) => { + this.project_id = project_id + if (error != null) { + throw error + } + return done() + } + ) + }) + }) + + describe('restoring a deleted doc', function() { + beforeEach(function(done) { + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + this.doc = _.find( + project.rootFolder[0].docs, + doc => doc.name === 'main.tex' + ) + return this.owner.request( + { + method: 'DELETE', + url: `/project/${this.project_id}/doc/${this.doc._id}` + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(204) + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/doc/${this.doc._id}/restore`, + json: { + name: 'main.tex' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + expect(body.doc_id).to.exist + this.restored_doc_id = body.doc_id + return done() + } + ) + } + ) + }) + }) + + return it('should have restored the doc', function(done) { + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + const restored_doc = _.find( + project.rootFolder[0].docs, + doc => doc.name === 'main.tex' + ) + expect(restored_doc._id.toString()).to.equal(this.restored_doc_id) + expect(this.doc._id).to.not.equal(this.restored_doc_id) + // console.log @doc_id, @restored_doc_id, MockDocstoreApi.docs[@project_id] + expect( + MockDocstoreApi.docs[this.project_id][this.restored_doc_id].lines + ).to.deep.equal( + MockDocstoreApi.docs[this.project_id][this.doc._id].lines + ) + return done() + }) + }) + }) + + return describe('restoring from v2 history', function() { + describe('restoring a text file', function() { + beforeEach(function(done) { + MockProjectHistoryApi.addOldFile( + this.project_id, + 42, + 'foo.tex', + 'hello world, this is foo.tex!' + ) + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/restore_file`, + json: { + pathname: 'foo.tex', + version: 42 + } + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return done() + } + ) + }) + + return it('should have created a doc', function(done) { + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + let doc = _.find( + project.rootFolder[0].docs, + doc => doc.name === 'foo.tex' + ) + doc = MockDocstoreApi.docs[this.project_id][doc._id] + expect(doc.lines).to.deep.equal(['hello world, this is foo.tex!']) + return done() + }) + }) + }) + + describe('restoring a binary file', function() { + beforeEach(function(done) { + this.pngData = fs.readFileSync( + Path.resolve(__dirname, '../files/1pixel.png'), + 'binary' + ) + MockProjectHistoryApi.addOldFile( + this.project_id, + 42, + 'image.png', + this.pngData + ) + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/restore_file`, + json: { + pathname: 'image.png', + version: 42 + } + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return done() + } + ) + }) + + return it('should have created a file', function(done) { + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + let file = _.find( + project.rootFolder[0].fileRefs, + file => file.name === 'image.png' + ) + file = MockFileStoreApi.files[this.project_id][file._id] + expect(file.content).to.equal(this.pngData) + return done() + }) + }) + }) + + describe('restoring to a directory that exists', function() { + beforeEach(function(done) { + MockProjectHistoryApi.addOldFile( + this.project_id, + 42, + 'foldername/foo2.tex', + 'hello world, this is foo-2.tex!' + ) + return this.owner.request.post( + { + uri: `project/${this.project_id}/folder`, + json: { + name: 'foldername' + } + }, + (error, response, body) => { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/restore_file`, + json: { + pathname: 'foldername/foo2.tex', + version: 42 + } + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return done() + } + ) + } + ) + }) + + return it('should have created the doc in the named folder', function(done) { + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + const folder = _.find( + project.rootFolder[0].folders, + folder => folder.name === 'foldername' + ) + let doc = _.find(folder.docs, doc => doc.name === 'foo2.tex') + doc = MockDocstoreApi.docs[this.project_id][doc._id] + expect(doc.lines).to.deep.equal(['hello world, this is foo-2.tex!']) + return done() + }) + }) + }) + + describe('restoring to a directory that no longer exists', function() { + beforeEach(function(done) { + MockProjectHistoryApi.addOldFile( + this.project_id, + 42, + 'nothere/foo3.tex', + 'hello world, this is foo-3.tex!' + ) + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/restore_file`, + json: { + pathname: 'nothere/foo3.tex', + version: 42 + } + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return done() + } + ) + }) + + return it('should have created the folder and restored the doc to it', function(done) { + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + const folder = _.find( + project.rootFolder[0].folders, + folder => folder.name === 'nothere' + ) + expect(folder).to.exist + let doc = _.find(folder.docs, doc => doc.name === 'foo3.tex') + doc = MockDocstoreApi.docs[this.project_id][doc._id] + expect(doc.lines).to.deep.equal(['hello world, this is foo-3.tex!']) + return done() + }) + }) + }) + + return describe('restoring to a filename that already exists', function() { + beforeEach(function(done) { + MockProjectHistoryApi.addOldFile( + this.project_id, + 42, + 'main.tex', + 'hello world, this is main.tex!' + ) + return this.owner.request( + { + method: 'POST', + url: `/project/${this.project_id}/restore_file`, + json: { + pathname: 'main.tex', + version: 42 + } + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return done() + } + ) + }) + + return it('should have created the doc in the root folder', function(done) { + return this.owner.getProject(this.project_id, (error, project) => { + if (error != null) { + throw error + } + let doc = _.find(project.rootFolder[0].docs, doc => + doc.name.match(/main \(Restored on/) + ) + expect(doc).to.exist + doc = MockDocstoreApi.docs[this.project_id][doc._id] + expect(doc.lines).to.deep.equal(['hello world, this is main.tex!']) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/SecurityHeadersTests.js b/services/web/test/acceptance/src/SecurityHeadersTests.js new file mode 100644 index 0000000000..a3d7f2aaf2 --- /dev/null +++ b/services/web/test/acceptance/src/SecurityHeadersTests.js @@ -0,0 +1,110 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { assert } = require('chai') +const async = require('async') +const User = require('./helpers/User') +const request = require('./helpers/request') + +const assert_has_common_headers = function(response) { + const { headers } = response + assert.equal(headers['x-download-options'], 'noopen') + assert.equal(headers['x-xss-protection'], '1; mode=block') + return assert.equal(headers['referrer-policy'], 'origin-when-cross-origin') +} + +const assert_has_cache_headers = function(response) { + const { headers } = response + assert.equal(headers['surrogate-control'], 'no-store') + assert.equal( + headers['cache-control'], + 'no-store, no-cache, must-revalidate, proxy-revalidate' + ) + assert.equal(headers['pragma'], 'no-cache') + return assert.equal(headers['expires'], '0') +} + +const assert_has_no_cache_headers = function(response) { + const { headers } = response + assert.isUndefined(headers['surrogate-control']) + assert.isUndefined(headers['cache-control']) + assert.isUndefined(headers['pragma']) + return assert.isUndefined(headers['expires']) +} + +describe('SecurityHeaders', function() { + before(function() { + return (this.user = new User()) + }) + + it('should not have x-powered-by header', done => + request.get('/', (err, res, body) => { + assert.isUndefined(res.headers['x-powered-by']) + return done() + })) + + it('should have all common headers', done => + request.get('/', (err, res, body) => { + assert_has_common_headers(res) + return done() + })) + + it('should not have cache headers on public pages', done => + request.get('/', (err, res, body) => { + assert_has_no_cache_headers(res) + return done() + })) + + it('should have cache headers when user is logged in', function(done) { + return async.series( + [ + cb => this.user.login(cb), + cb => this.user.request.get('/', cb), + cb => this.user.logout(cb) + ], + (err, results) => { + const main_response = results[1][0] + assert_has_cache_headers(main_response) + return done() + } + ) + }) + + return it('should have cache headers on project page', function(done) { + return async.series( + [ + cb => this.user.login(cb), + cb => { + return this.user.createProject( + 'public-project', + (error, project_id) => { + if (error != null) { + return done(error) + } + this.project_id = project_id + return this.user.makePublic(this.project_id, 'readAndWrite', cb) + } + ) + }, + cb => this.user.logout(cb) + ], + (err, results) => { + return request.get(`/project/${this.project_id}`, (err, res, body) => { + assert_has_cache_headers(res) + return done() + }) + } + ) + }) +}) diff --git a/services/web/test/acceptance/src/SessionTests.js b/services/web/test/acceptance/src/SessionTests.js new file mode 100644 index 0000000000..e26e160ddf --- /dev/null +++ b/services/web/test/acceptance/src/SessionTests.js @@ -0,0 +1,490 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const User = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') +const redis = require('./helpers/redis') +const MockV1Api = require('./helpers/MockV1Api') + +describe('Sessions', function() { + before(function(done) { + this.timeout(20000) + this.user1 = new User() + this.site_admin = new User({ email: 'admin@example.com' }) + return async.series( + [cb => this.user1.login(cb), cb => this.user1.logout(cb)], + done + ) + }) + + describe('one session', () => + it('should have one session in UserSessions set', function(done) { + return async.series( + [ + next => { + return redis.clearUserSessions(this.user1, next) + }, + + // login, should add session to set + next => { + return this.user1.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(1) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // should be able to access project list page + next => { + return this.user1.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(200) + return next() + }) + }, + + // logout, should remove session from set + next => { + return this.user1.logout(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(0) + return next() + }) + } + ], + (err, result) => { + if (err) { + throw err + } + return done() + } + ) + })) + + describe('two sessions', function() { + before(function() { + // set up second session for this user + this.user2 = new User() + this.user2.email = this.user1.email + return (this.user2.password = this.user1.password) + }) + + return it('should have two sessions in UserSessions set', function(done) { + return async.series( + [ + next => { + return redis.clearUserSessions(this.user1, next) + }, + + // login, should add session to set + next => { + return this.user1.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(1) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // login again, should add the second session to set + next => { + return this.user2.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(2) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + expect(sessions[1].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // both should be able to access project list page + next => { + return this.user1.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(200) + return next() + }) + }, + + next => { + return this.user2.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(200) + return next() + }) + }, + + // logout first session, should remove session from set + next => { + return this.user1.logout(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(1) + return next() + }) + }, + + // first session should not have access to project list page + next => { + return this.user1.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(302) + return next() + }) + }, + + // second session should still have access to settings + next => { + return this.user2.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(200) + return next() + }) + }, + + // logout second session, should remove last session from set + next => { + return this.user2.logout(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(0) + return next() + }) + }, + + // second session should not have access to project list page + next => { + return this.user2.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(302) + return next() + }) + } + ], + (err, result) => { + if (err) { + throw err + } + return done() + } + ) + }) + }) + + describe('three sessions, password reset', function() { + before(function() { + // set up second session for this user + this.user2 = new User() + this.user2.email = this.user1.email + this.user2.password = this.user1.password + this.user3 = new User() + this.user3.email = this.user1.email + return (this.user3.password = this.user1.password) + }) + + return it('should erase both sessions when password is reset', function(done) { + return async.series( + [ + next => { + return redis.clearUserSessions(this.user1, next) + }, + + // login, should add session to set + next => { + return this.user1.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(1) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // login again, should add the second session to set + next => { + return this.user2.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(2) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + expect(sessions[1].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // login third session, should add the second session to set + next => { + return this.user3.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(3) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + expect(sessions[1].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // password reset from second session, should erase two of the three sessions + next => { + return this.user2.changePassword(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user2, (err, sessions) => { + expect(sessions.length).to.equal(1) + return next() + }) + }, + + // users one and three should not be able to access project list page + next => { + return this.user1.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(302) + return next() + }) + }, + + next => { + return this.user3.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(302) + return next() + }) + }, + + // user two should still be logged in, and able to access project list page + next => { + return this.user2.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(200) + return next() + }) + }, + + // logout second session, should remove last session from set + next => { + return this.user2.logout(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(0) + return next() + }) + } + ], + (err, result) => { + if (err) { + throw err + } + return done() + } + ) + }) + }) + + return describe('three sessions, sessions page', function() { + before(function(done) { + // set up second session for this user + this.user2 = new User() + this.user2.email = this.user1.email + this.user2.password = this.user1.password + this.user3 = new User() + this.user3.email = this.user1.email + this.user3.password = this.user1.password + return async.series( + [ + this.user2.login.bind(this.user2), + this.user2.activateSudoMode.bind(this.user2) + ], + done + ) + }) + + return it('should allow the user to erase the other two sessions', function(done) { + return async.series( + [ + next => { + return redis.clearUserSessions(this.user1, next) + }, + + // login, should add session to set + next => { + return this.user1.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(1) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // login again, should add the second session to set + next => { + return this.user2.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(2) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + expect(sessions[1].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // login third session, should add the second session to set + next => { + return this.user3.login(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(3) + expect(sessions[0].slice(0, 5)).to.equal('sess:') + expect(sessions[1].slice(0, 5)).to.equal('sess:') + return next() + }) + }, + + // enter sudo-mode + next => { + return this.user2.getCsrfToken(err => { + expect(err).to.be.oneOf([null, undefined]) + return this.user2.request.post( + { + uri: '/confirm-password', + json: { + password: this.user2.password + } + }, + (err, response, body) => { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + return next() + } + ) + }) + }, + + // check the sessions page + next => { + return this.user2.request.get( + { + uri: '/user/sessions' + }, + (err, response, body) => { + expect(err).to.be.oneOf([null, undefined]) + expect(response.statusCode).to.equal(200) + return next() + } + ) + }, + + // clear sessions from second session, should erase two of the three sessions + next => { + return this.user2.getCsrfToken(err => { + expect(err).to.be.oneOf([null, undefined]) + return this.user2.request.post( + { + uri: '/user/sessions/clear' + }, + err => next(err) + ) + }) + }, + + next => { + return redis.getUserSessions(this.user2, (err, sessions) => { + expect(sessions.length).to.equal(1) + return next() + }) + }, + + // users one and three should not be able to access project list page + next => { + return this.user1.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(302) + return next() + }) + }, + + next => { + return this.user3.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(302) + return next() + }) + }, + + // user two should still be logged in, and able to access project list page + next => { + return this.user2.getProjectListPage((err, statusCode) => { + expect(err).to.equal(null) + expect(statusCode).to.equal(200) + return next() + }) + }, + + // logout second session, should remove last session from set + next => { + return this.user2.logout(err => next(err)) + }, + + next => { + return redis.getUserSessions(this.user1, (err, sessions) => { + expect(sessions.length).to.equal(0) + return next() + }) + } + ], + (err, result) => { + if (err) { + throw err + } + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/acceptance/src/SettingsTests.js b/services/web/test/acceptance/src/SettingsTests.js new file mode 100644 index 0000000000..3df635e637 --- /dev/null +++ b/services/web/test/acceptance/src/SettingsTests.js @@ -0,0 +1,63 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const async = require('async') +const User = require('./helpers/User') +const MockV1Api = require('./helpers/MockV1Api') + +describe('SettingsPage', function() { + before(function(done) { + this.user = new User() + this.v1Id = 1234 + this.v1User = { + id: this.v1Id, + email: this.user.email, + password: this.user.password, + profile: { + id: this.v1Id, + email: this.user.email + } + } + return async.series( + [ + this.user.ensureUserExists.bind(this.user), + this.user.login.bind(this.user), + cb => this.user.mongoUpdate({ $set: { 'overleaf.id': this.v1Id } }, cb), + cb => { + MockV1Api.setUser(this.v1Id, this.v1User) + return cb() + }, + this.user.activateSudoMode.bind(this.user) + ], + done + ) + }) + + it('load settings page', function(done) { + return this.user.getUserSettingsPage(function(err, statusCode) { + statusCode.should.equal(200) + return done() + }) + }) + + return it('update main email address', function(done) { + const newEmail = 'foo@bar.com' + return this.user.updateSettings({ email: newEmail }, error => { + should.not.exist(error) + return this.user.get(function(error, user) { + user.email.should.equal(newEmail) + user.emails.length.should.equal(1) + user.emails[0].email.should.equal(newEmail) + return done() + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/SubscriptionTests.js b/services/web/test/acceptance/src/SubscriptionTests.js new file mode 100644 index 0000000000..a94340bf24 --- /dev/null +++ b/services/web/test/acceptance/src/SubscriptionTests.js @@ -0,0 +1,590 @@ +/* eslint-disable + handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const User = require('./helpers/User') +const { Subscription } = require('../../../app/src/models/Subscription') +const { Institution } = require('../../../app/src/models/Institution') +const SubscriptionViewModelBuilder = require('../../../app/src/Features/Subscription/SubscriptionViewModelBuilder') + +const MockRecurlyApi = require('./helpers/MockRecurlyApi') +const MockV1Api = require('./helpers/MockV1Api') + +describe('Subscriptions', function() { + describe('dashboard', function() { + before(function(done) { + this.user = new User() + return this.user.ensureUserExists(done) + }) + + describe('when the user has no subscription', function() { + before(function(done) { + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + }) + + it('should return no personalSubscription', function() { + return expect(this.data.personalSubscription).to.equal(null) + }) + + return it('should return no memberGroupSubscriptions', function() { + return expect(this.data.memberGroupSubscriptions).to.deep.equal([]) + }) + }) + + describe('when the user has a subscription with recurly', function() { + before(function(done) { + MockRecurlyApi.accounts['mock-account-id'] = this.accounts = { + hosted_login_token: 'mock-login-token' + } + MockRecurlyApi.subscriptions[ + 'mock-subscription-id' + ] = this.subscription = { + plan_code: 'collaborator', + tax_in_cents: 100, + tax_rate: 0.2, + unit_amount_in_cents: 500, + currency: 'GBP', + current_period_ends_at: new Date(2018, 4, 5), + state: 'active', + account_id: 'mock-account-id', + trial_ends_at: new Date(2018, 6, 7) + } + MockRecurlyApi.coupons = this.coupons = { + 'test-coupon-1': { description: 'Test Coupon 1' }, + 'test-coupon-2': { description: 'Test Coupon 2' }, + 'test-coupon-3': { name: 'TestCoupon3' } + } + Subscription.create( + { + admin_id: this.user._id, + manager_ids: [this.user._id], + recurlySubscription_id: 'mock-subscription-id', + planCode: 'collaborator' + }, + error => { + if (error != null) { + return done(error) + } + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + }) + + after(function(done) { + MockRecurlyApi.accounts = {} + MockRecurlyApi.subscriptions = {} + MockRecurlyApi.coupons = {} + MockRecurlyApi.redemptions = {} + Subscription.remove( + { + admin_id: this.user._id + }, + done + ) + }) + + it('should return a personalSubscription with populated recurly data', function() { + const subscription = this.data.personalSubscription + expect(subscription).to.exist + expect(subscription.planCode).to.equal('collaborator') + expect(subscription.recurly).to.exist + return expect(subscription.recurly).to.deep.equal({ + activeCoupons: [], + billingDetailsLink: + 'https://test.recurly.com/account/billing_info/edit?ht=mock-login-token', + currency: 'GBP', + nextPaymentDueAt: '5th May 2018', + price: '£6.00', + state: 'active', + tax: 100, + taxRate: 0.2, + trial_ends_at: new Date(2018, 6, 7), + trialEndsAtFormatted: '7th July 2018' + }) + }) + + it('should return no memberGroupSubscriptions', function() { + return expect(this.data.memberGroupSubscriptions).to.deep.equal([]) + }) + + return it('should include redeemed coupons', function(done) { + MockRecurlyApi.redemptions['mock-account-id'] = [ + { state: 'active', coupon_code: 'test-coupon-1' }, + { state: 'inactive', coupon_code: 'test-coupon-2' }, + { state: 'active', coupon_code: 'test-coupon-3' } + ] + + // rebuild the view model with the redemptions + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + function(error, data) { + expect(error).to.not.exist + expect( + data.personalSubscription.recurly.activeCoupons + ).to.deep.equal([ + { + coupon_code: 'test-coupon-1', + name: '', + description: 'Test Coupon 1' + }, + { + coupon_code: 'test-coupon-3', + name: 'TestCoupon3', + description: '' + } + ]) + return done() + } + ) + }) + }) + + describe('when the user has a subscription without recurly', function() { + before(function(done) { + Subscription.create( + { + admin_id: this.user._id, + manager_ids: [this.user._id], + planCode: 'collaborator' + }, + error => { + if (error != null) { + return done(error) + } + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + }) + + after(function(done) { + Subscription.remove( + { + admin_id: this.user._id + }, + done + ) + }) + + it('should return a personalSubscription with no recurly data', function() { + const subscription = this.data.personalSubscription + expect(subscription).to.exist + expect(subscription.planCode).to.equal('collaborator') + return expect(subscription.recurly).to.not.exist + }) + + return it('should return no memberGroupSubscriptions', function() { + return expect(this.data.memberGroupSubscriptions).to.deep.equal([]) + }) + }) + + describe('when the user is a member of a group subscription', function() { + before(function(done) { + this.owner1 = new User() + this.owner2 = new User() + async.series( + [ + cb => this.owner1.ensureUserExists(cb), + cb => this.owner2.ensureUserExists(cb), + cb => + Subscription.create( + { + admin_id: this.owner1._id, + manager_ids: [this.owner1._id], + planCode: 'collaborator', + groupPlan: true, + member_ids: [this.user._id] + }, + cb + ), + cb => + Subscription.create( + { + admin_id: this.owner2._id, + manager_ids: [this.owner2._id], + planCode: 'collaborator', + groupPlan: true, + member_ids: [this.user._id] + }, + cb + ) + ], + error => { + if (error != null) { + return done(error) + } + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + }) + + after(function(done) { + Subscription.remove( + { + admin_id: this.owner1._id + }, + error => { + if (error != null) { + return done(error) + } + return Subscription.remove( + { + admin_id: this.owner2._id + }, + done + ) + } + ) + }) + + it('should return no personalSubscription', function() { + return expect(this.data.personalSubscription).to.equal(null) + }) + + return it('should return the two memberGroupSubscriptions', function() { + expect(this.data.memberGroupSubscriptions.length).to.equal(2) + expect( + // Mongoose populates the admin_id with the user + this.data.memberGroupSubscriptions[0].admin_id._id.toString() + ).to.equal(this.owner1._id) + return expect( + this.data.memberGroupSubscriptions[1].admin_id._id.toString() + ).to.equal(this.owner2._id) + }) + }) + + describe('when the user is a manager of a group subscription', function() { + before(function(done) { + this.owner1 = new User() + this.owner2 = new User() + async.series( + [ + cb => this.owner1.ensureUserExists(cb), + cb => this.owner2.ensureUserExists(cb), + cb => + Subscription.create( + { + admin_id: this.owner1._id, + manager_ids: [this.owner1._id, this.user._id], + planCode: 'collaborator', + groupPlan: true + }, + cb + ) + ], + error => { + if (error != null) { + return done(error) + } + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + }) + + after(function(done) { + Subscription.remove( + { + admin_id: this.owner1._id + }, + done + ) + }) + + it('should return no personalSubscription', function() { + return expect(this.data.personalSubscription).to.equal(null) + }) + + return it('should return the managedGroupSubscriptions', function() { + expect(this.data.managedGroupSubscriptions.length).to.equal(1) + const subscription = this.data.managedGroupSubscriptions[0] + expect( + // Mongoose populates the admin_id with the user + subscription.admin_id._id.toString() + ).to.equal(this.owner1._id) + return expect(subscription.groupPlan).to.equal(true) + }) + }) + + describe('when the user is a manager of an institution', function() { + before(function(done) { + this.v1Id = MockV1Api.nextV1Id() + async.series( + [ + cb => { + return Institution.create( + { + v1Id: this.v1Id, + managerIds: [this.user._id] + }, + cb + ) + } + ], + error => { + if (error != null) { + return done(error) + } + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + }) + + after(function(done) { + Institution.remove( + { + v1Id: this.v1Id + }, + done + ) + }) + + return it('should return the managedInstitutions', function() { + expect(this.data.managedInstitutions.length).to.equal(1) + const institution = this.data.managedInstitutions[0] + expect(institution.v1Id).to.equal(this.v1Id) + return expect(institution.name).to.equal(`Institution ${this.v1Id}`) + }) + }) + + describe('when the user is a member of an affiliation', function() { + before(function(done) { + const v1Id = MockV1Api.nextV1Id() + MockV1Api.setUser(v1Id, { + subscription: {}, + subscription_status: {} + }) + MockV1Api.setAffiliations([ + { + email: 'confirmed-affiliation-email@stanford.example.edu', + institution: { + name: 'Stanford', + licence: 'pro_plus', + confirmed: true + } + }, + { + email: 'unconfirmed-affiliation-email@harvard.example.edu', + institution: { + name: 'Harvard', + licence: 'pro_plus', + confirmed: true + } + }, + { + email: 'confirmed-affiliation-email@mit.example.edu', + institution: { name: 'MIT', licence: 'pro_plus', confirmed: false } + } + ]) + return async.series( + [ + cb => { + return this.user.setV1Id(v1Id, cb) + }, + cb => { + return this.user.addEmail( + 'unconfirmed-affiliation-email@harvard.example.edu', + cb + ) + }, + cb => { + return this.user.addEmail( + 'confirmed-affiliation-email@stanford.example.edu', + cb + ) + }, + cb => { + return this.user.confirmEmail( + 'confirmed-affiliation-email@stanford.example.edu', + cb + ) + }, + cb => { + return this.user.addEmail( + 'confirmed-affiliation-email@mit.example.edu', + cb + ) + }, + cb => { + return this.user.confirmEmail( + 'confirmed-affiliation-email@mit.example.edu', + cb + ) + } + ], + error => { + if (error != null) { + return done(error) + } + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + }) + + return it('should return only the affilations with confirmed institutions, and confirmed emails', function() { + return expect(this.data.confirmedMemberInstitutions).to.deep.equal([ + { name: 'Stanford', licence: 'pro_plus', confirmed: true } + ]) + }) + }) + + return describe('when the user has a v1 subscription', function() { + before(function(done) { + let v1Id + MockV1Api.setUser((v1Id = MockV1Api.nextV1Id()), { + subscription: (this.subscription = { + trial: false, + has_plan: true, + teams: [ + { + id: 56, + name: 'Test team' + } + ] + }), + subscription_status: (this.subscription_status = { + product: { mock: 'product' }, + team: null + }) + }) + return this.user.setV1Id(v1Id, error => { + if (error != null) { + return done(error) + } + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + this.user, + (error, data) => { + this.data = data + if (error != null) { + return done(error) + } + return done() + } + ) + }) + }) + + it('should return no personalSubscription', function() { + return expect(this.data.personalSubscription).to.equal(null) + }) + + it('should return no memberGroupSubscriptions', function() { + return expect(this.data.memberGroupSubscriptions).to.deep.equal([]) + }) + + return it('should return a v1SubscriptionStatus', function() { + return expect(this.data.v1SubscriptionStatus).to.deep.equal( + this.subscription_status + ) + }) + }) + }) + + return describe('canceling', function() { + before(function(done) { + let v1Id + this.user = new User() + MockV1Api.setUser((v1Id = MockV1Api.nextV1Id()), (this.v1_user = {})) + return async.series( + [cb => this.user.login(cb), cb => this.user.setV1Id(v1Id, cb)], + error => { + return this.user.request( + { + method: 'POST', + url: '/user/subscription/v1/cancel' + }, + (error, response) => { + this.response = response + if (error != null) { + return done(error) + } + return done() + } + ) + } + ) + }) + + it('should tell v1 to cancel the subscription', function() { + return expect(this.v1_user.canceled).to.equal(true) + }) + + return it('should redirect to the subscription dashboard', function() { + expect(this.response.statusCode).to.equal(302) + return expect(this.response.headers.location).to.equal( + '/user/subscription' + ) + }) + }) +}) diff --git a/services/web/test/acceptance/src/TokenAccessTests.js b/services/web/test/acceptance/src/TokenAccessTests.js new file mode 100644 index 0000000000..b6089a33b1 --- /dev/null +++ b/services/web/test/acceptance/src/TokenAccessTests.js @@ -0,0 +1,979 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const MockV1Api = require('./helpers/MockV1Api') +const User = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') +const { db, ObjectId } = require('../../../app/src/infrastructure/mongojs') + +const try_read_access = (user, project_id, test, callback) => + async.series( + [ + cb => + user.request.get(`/project/${project_id}`, function( + error, + response, + body + ) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + }), + cb => + user.request.get(`/project/${project_id}/download/zip`, function( + error, + response, + body + ) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + }) + ], + callback + ) + +const try_read_only_token_access = (user, token, test, callback) => + async.series( + [ + cb => + user.request.get(`/read/${token}`, function(error, response, body) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + }) + ], + callback + ) + +const try_read_and_write_token_access = (user, token, test, callback) => + async.series( + [ + cb => + user.request.get(`/${token}`, function(error, response, body) { + if (error != null) { + return cb(error) + } + test(response, body) + return cb() + }) + ], + callback + ) + +const try_content_access = function(user, project_id, test, callback) { + // The real-time service calls this end point to determine the user's + // permissions. + let user_id + if (user.id != null) { + user_id = user.id + } else { + user_id = 'anonymous-user' + } + return request.post( + { + url: `/project/${project_id}/join`, + qs: { user_id }, + auth: { + user: settings.apis.web.user, + pass: settings.apis.web.pass, + sendImmediately: true + }, + json: true, + jar: false + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + test(response, body) + return callback() + } + ) +} + +const try_anon_content_access = function( + user, + project_id, + token, + test, + callback +) { + // The real-time service calls this end point to determine the user's + // permissions. + let user_id + if (user.id != null) { + user_id = user.id + } else { + user_id = 'anonymous-user' + } + return request.post( + { + url: `/project/${project_id}/join`, + qs: { user_id }, + auth: { + user: settings.apis.web.user, + pass: settings.apis.web.pass, + sendImmediately: true + }, + headers: { + 'x-sl-anonymous-access-token': token + }, + json: true, + jar: false + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + test(response, body) + return callback() + } + ) +} + +describe('TokenAccess', function() { + before(function(done) { + this.timeout(90000) + this.owner = new User() + this.other1 = new User() + this.other2 = new User() + this.anon = new User() + return async.parallel( + [ + cb => this.owner.login(cb), + cb => this.other1.login(cb), + cb => this.other2.login(cb), + cb => this.anon.getCsrfToken(cb) + ], + done + ) + }) + + describe('no token-access', function() { + before(function(done) { + return this.owner.createProject( + `token-ro-test${Math.random()}`, + (err, project_id) => { + if (err != null) { + return done(err) + } + this.project_id = project_id + // Note, never made token-based, + // thus no tokens + return done() + } + ) + }) + + it('should deny access ', function(done) { + return try_read_access( + this.other1, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + return it('should not allow the user to join the project', function(done) { + return try_content_access( + this.other1, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + + describe('read-only token', function() { + before(function(done) { + return this.owner.createProject( + `token-ro-test${Math.random()}`, + (err, project_id) => { + if (err != null) { + return done(err) + } + this.project_id = project_id + return this.owner.makeTokenBased(this.project_id, err => { + if (err != null) { + return done(err) + } + return this.owner.getProject(this.project_id, (err, project) => { + if (err != null) { + return done(err) + } + this.tokens = project.tokens + return done() + }) + }) + } + ) + }) + + it('should deny access before the token is used', function(done) { + return try_read_access( + this.other1, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should allow the user to access project via read-only token url', function(done) { + return try_read_only_token_access( + this.other1, + this.tokens.readOnly, + (response, body) => { + return expect(response.statusCode).to.equal(200) + }, + done + ) + }) + + it('should allow the user to join the project with read-only access', function(done) { + return try_content_access( + this.other1, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal('readOnly') + }, + done + ) + }) + + return describe('made private again', function() { + before(function(done) { + return this.owner.makePrivate(this.project_id, () => + setTimeout(done, 1000) + ) + }) + + it('should deny access to project', function(done) { + return try_read_access( + this.other1, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should not allow the user to access read-only token', function(done) { + return try_read_only_token_access( + this.other1, + this.tokens.readOnly, + (response, body) => { + return expect(response.statusCode).to.equal(404) + }, + done + ) + }) + + return it('should not allow the user to join the project', function(done) { + return try_content_access( + this.other1, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + }) + + describe('anonymous read-only token', function() { + before(function(done) { + return this.owner.createProject( + `token-anon-ro-test${Math.random()}`, + (err, project_id) => { + if (err != null) { + return done(err) + } + this.project_id = project_id + return this.owner.makeTokenBased(this.project_id, err => { + if (err != null) { + return done(err) + } + return this.owner.getProject(this.project_id, (err, project) => { + if (err != null) { + return done(err) + } + this.tokens = project.tokens + return done() + }) + }) + } + ) + }) + + it('should deny access before the token is used', function(done) { + return try_read_access( + this.anon, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should allow the user to access project via read-only token url', function(done) { + return try_read_only_token_access( + this.anon, + this.tokens.readOnly, + (response, body) => { + return expect(response.statusCode).to.equal(200) + }, + done + ) + }) + + it('should allow the user to anonymously join the project with read-only access', function(done) { + return try_anon_content_access( + this.anon, + this.project_id, + this.tokens.readOnly, + (response, body) => { + return expect(body.privilegeLevel).to.equal('readOnly') + }, + done + ) + }) + + return describe('made private again', function() { + before(function(done) { + return this.owner.makePrivate(this.project_id, () => + setTimeout(done, 1000) + ) + }) + + it('should deny access to project', function(done) { + return try_read_access( + this.anon, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should not allow the user to access read-only token', function(done) { + return try_read_only_token_access( + this.anon, + this.tokens.readOnly, + (response, body) => { + return expect(response.statusCode).to.equal(404) + }, + done + ) + }) + + return it('should not allow the user to join the project', function(done) { + return try_anon_content_access( + this.anon, + this.project_id, + this.tokens.readOnly, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + }) + + describe('read-and-write token', function() { + before(function(done) { + return this.owner.createProject( + `token-rw-test${Math.random()}`, + (err, project_id) => { + if (err != null) { + return done(err) + } + this.project_id = project_id + return this.owner.makeTokenBased(this.project_id, err => { + if (err != null) { + return done(err) + } + return this.owner.getProject(this.project_id, (err, project) => { + if (err != null) { + return done(err) + } + this.tokens = project.tokens + return done() + }) + }) + } + ) + }) + + it('should deny access before the token is used', function(done) { + return try_read_access( + this.other1, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + expect(response.headers.location).to.match(/\/restricted.*/) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should allow the user to access project via read-and-write token url', function(done) { + return try_read_and_write_token_access( + this.other1, + this.tokens.readAndWrite, + (response, body) => { + return expect(response.statusCode).to.equal(200) + }, + done + ) + }) + + it('should allow the user to join the project with read-and-write access', function(done) { + return try_content_access( + this.other1, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal('readAndWrite') + }, + done + ) + }) + + return describe('made private again', function() { + before(function(done) { + return this.owner.makePrivate(this.project_id, () => + setTimeout(done, 1000) + ) + }) + + it('should deny access to project', function(done) { + return try_read_access( + this.other1, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should not allow the user to access read-and-write token', function(done) { + return try_read_and_write_token_access( + this.other1, + this.tokens.readAndWrite, + (response, body) => { + return expect(response.statusCode).to.equal(404) + }, + done + ) + }) + + return it('should not allow the user to join the project', function(done) { + return try_content_access( + this.other1, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + }) + + if (!settings.allowAnonymousReadAndWriteSharing) { + describe('anonymous read-and-write token, disabled', function() { + before(function(done) { + return this.owner.createProject( + `token-anon-rw-test${Math.random()}`, + (err, project_id) => { + if (err != null) { + return done(err) + } + this.project_id = project_id + return this.owner.makeTokenBased(this.project_id, err => { + if (err != null) { + return done(err) + } + return this.owner.getProject(this.project_id, (err, project) => { + if (err != null) { + return done(err) + } + this.tokens = project.tokens + return done() + }) + }) + } + ) + }) + + it('should deny access before the token is used', function(done) { + return try_read_access( + this.anon, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should not allow the user to access read-and-write token', function(done) { + return try_read_and_write_token_access( + this.anon, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + return it('should not allow the user to join the project', function(done) { + return try_anon_content_access( + this.anon, + this.project_id, + this.tokens.readAndWrite, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + } else { + describe('anonymous read-and-write token, enabled', function() { + before(function(done) { + return this.owner.createProject( + `token-anon-rw-test${Math.random()}`, + (err, project_id) => { + if (err != null) { + return done(err) + } + this.project_id = project_id + return this.owner.makeTokenBased(this.project_id, err => { + if (err != null) { + return done(err) + } + return this.owner.getProject(this.project_id, (err, project) => { + if (err != null) { + return done(err) + } + this.tokens = project.tokens + return done() + }) + }) + } + ) + }) + + it('should deny access before the token is used', function(done) { + return try_read_access( + this.anon, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should allow the user to access project via read-and-write token url', function(done) { + return try_read_and_write_token_access( + this.anon, + this.tokens.readAndWrite, + (response, body) => { + return expect(response.statusCode).to.equal(200) + }, + done + ) + }) + + it('should allow the user to anonymously join the project with read-and-write access', function(done) { + return try_anon_content_access( + this.anon, + this.project_id, + this.tokens.readAndWrite, + (response, body) => { + return expect(body.privilegeLevel).to.equal('readAndWrite') + }, + done + ) + }) + + return describe('made private again', function() { + before(function(done) { + return this.owner.makePrivate(this.project_id, () => + setTimeout(done, 1000) + ) + }) + + it('should deny access to project', function(done) { + return try_read_access( + this.anon, + this.project_id, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(body).to.match(/.*\/restricted.*/) + }, + done + ) + }) + + it('should not allow the user to access read-and-write token', function(done) { + return try_read_and_write_token_access( + this.anon, + this.tokens.readAndWrite, + (response, body) => { + return expect(response.statusCode).to.equal(404) + }, + done + ) + }) + + return it('should not allow the user to join the project', function(done) { + return try_anon_content_access( + this.anon, + this.project_id, + this.tokens.readAndWrite, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + }) + } + + describe('private overleaf project', function() { + before(function(done) { + return this.owner.createProject('overleaf-import', (err, project_id) => { + this.project_id = project_id + return this.owner.makeTokenBased(this.project_id, err => { + return this.owner.getProject(this.project_id, (err, project) => { + this.tokens = project.tokens + return this.owner.makePrivate(this.project_id, () => { + return db.projects.update( + { _id: project._id }, + { + $set: { + overleaf: { id: 1234 } + } + }, + err => { + return done() + } + ) + }) + }) + }) + }) + }) + + it('should redirect to canonical path, when owner uses read-write token', function(done) { + return try_read_and_write_token_access( + this.owner, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.equal( + `/project/${this.project_id}` + ) + }, + done + ) + }) + + it('should allow the owner access to the project', function(done) { + return try_read_access( + this.owner, + this.project_id, + (response, body) => { + return expect(response.statusCode).to.equal(200) + }, + done + ) + }) + + it('should allow owner to join the project', function(done) { + return try_content_access( + this.owner, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal('owner') + }, + done + ) + }) + + return it('should not allow other user to join the project', function(done) { + return try_content_access( + this.other2, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + + describe('private project, with higher access', function() { + before(function(done) { + return this.owner.createProject( + `higher-access-test-${Math.random()}`, + (err, project_id) => { + this.project_id = project_id + return this.owner.addUserToProject( + this.project_id, + this.other1, + 'readAndWrite', + err => { + return this.owner.makeTokenBased(this.project_id, err => { + return this.owner.getProject( + this.project_id, + (err, project) => { + this.tokens = project.tokens + return this.owner.makePrivate(this.project_id, () => { + return setTimeout(done, 1000) + }) + } + ) + }) + } + ) + } + ) + }) + + it('should redirect to canonical path, when user uses read-write token', function(done) { + return try_read_and_write_token_access( + this.other1, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.equal( + `/project/${this.project_id}` + ) + }, + done + ) + }) + + it('should redirect to canonical path, when user uses read-only token', function(done) { + return try_read_only_token_access( + this.other1, + this.tokens.readOnly, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.equal( + `/project/${this.project_id}` + ) + }, + done + ) + }) + + it('should allow the user access to the project', function(done) { + return try_read_access( + this.other1, + this.project_id, + (response, body) => { + return expect(response.statusCode).to.equal(200) + }, + done + ) + }) + + it('should allow user to join the project', function(done) { + return try_content_access( + this.other1, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal('readAndWrite') + }, + done + ) + }) + + return it('should not allow a different user to join the project', function(done) { + return try_content_access( + this.other2, + this.project_id, + (response, body) => { + return expect(body.privilegeLevel).to.equal(false) + }, + done + ) + }) + }) + + describe('unimported v1 project', function() { + before(() => (settings.overleaf = { host: 'http://localhost:5000' })) + + after(() => delete settings.overleaf) + + it('should redirect read and write token to v1', function(done) { + const unimportedV1Token = '123abc' + return try_read_and_write_token_access( + this.owner, + unimportedV1Token, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.equal( + '/sign_in_to_v1?return_to=/123abc' + ) + }, + done + ) + }) + + return it('should redirect read only token to v1', function(done) { + const unimportedV1Token = 'abcd' + return try_read_only_token_access( + this.owner, + unimportedV1Token, + (response, body) => { + expect(response.statusCode).to.equal(302) + return expect(response.headers.location).to.equal( + '/sign_in_to_v1?return_to=/read/abcd' + ) + }, + done + ) + }) + }) + + return describe('importing v1 project', function() { + before(function(done) { + settings.projectImportingCheckMaxCreateDelta = 3600 + settings.overleaf = { host: 'http://localhost:5000' } + return this.owner.createProject( + `token-rw-test${Math.random()}`, + (err, project_id) => { + if (err != null) { + return done(err) + } + this.project_id = project_id + return this.owner.makeTokenBased(this.project_id, err => { + if (err != null) { + return done(err) + } + return db.projects.update( + { _id: ObjectId(project_id) }, + { $set: { overleaf: { id: 1234 } } }, + err => { + if (err != null) { + return done(err) + } + return this.owner.getProject( + this.project_id, + (err, project) => { + if (err != null) { + return done(err) + } + this.tokens = project.tokens + MockV1Api.setDocExported(this.tokens.readAndWrite, { + exporting: true + }) + MockV1Api.setDocExported(this.tokens.readOnly, { + exporting: true + }) + return done() + } + ) + } + ) + }) + } + ) + }) + + after(function() { + delete settings.projectImportingCheckMaxCreateDelta + return delete settings.overleaf + }) + + it('should show importing page for read and write token', function(done) { + return try_read_and_write_token_access( + this.owner, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(200) + return expect(body).to.include('ImportingController') + }, + done + ) + }) + + it('should show importing page for read only token', function(done) { + return try_read_only_token_access( + this.owner, + this.tokens.readOnly, + (response, body) => { + expect(response.statusCode).to.equal(200) + return expect(body).to.include('ImportingController') + }, + done + ) + }) + + return describe('when importing check not configured', function() { + before(() => delete settings.projectImportingCheckMaxCreateDelta) + + return it('should load editor', function(done) { + return try_read_and_write_token_access( + this.owner, + this.tokens.readAndWrite, + (response, body) => { + expect(response.statusCode).to.equal(200) + return expect(body).to.include('IdeController') + }, + done + ) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/TpdsUpdateTests.js b/services/web/test/acceptance/src/TpdsUpdateTests.js new file mode 100644 index 0000000000..dead7da3e2 --- /dev/null +++ b/services/web/test/acceptance/src/TpdsUpdateTests.js @@ -0,0 +1,80 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js') +const request = require('./helpers/request') +const User = require('./helpers/User') + +describe('TpdsUpdateTests', function() { + before(function(done) { + this.owner = new User() + return this.owner.login(error => { + if (error != null) { + throw error + } + return this.owner.createProject( + 'test-project', + { template: 'example' }, + (error, project_id) => { + if (error != null) { + throw error + } + this.project_id = project_id + return done() + } + ) + }) + }) + + return describe('deleting a file', function() { + before(function(done) { + return request( + { + method: 'DELETE', + url: `/project/${this.project_id}/contents/main.tex`, + auth: { + username: 'sharelatex', + password: 'password', + sendImmediately: true + } + }, + function(error, response, body) { + if (error != null) { + throw error + } + expect(response.statusCode).to.equal(200) + return done() + } + ) + }) + + return it('should have deleted the file', function(done) { + return ProjectGetter.getProject(this.project_id, function( + error, + project + ) { + if (error != null) { + throw error + } + const projectFolder = project.rootFolder[0] + for (let doc of Array.from(projectFolder.docs)) { + if (doc.name === 'main.tex') { + throw new Error('expected main.tex to have been deleted') + } + } + return done() + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/UserEmailsTests.js b/services/web/test/acceptance/src/UserEmailsTests.js new file mode 100644 index 0000000000..ffa6cf7658 --- /dev/null +++ b/services/web/test/acceptance/src/UserEmailsTests.js @@ -0,0 +1,824 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const async = require('async') +const User = require('./helpers/User') +const request = require('./helpers/request') +const settings = require('settings-sharelatex') +const { db, ObjectId } = require('../../../app/src/infrastructure/mongojs') +const MockV1Api = require('./helpers/MockV1Api') + +describe('UserEmails', function() { + beforeEach(function(done) { + this.timeout(20000) + this.user = new User() + return this.user.login(done) + }) + + describe('confirming an email', function() { + it('should confirm the email', function(done) { + let token = null + return async.series( + [ + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { + email: 'newly-added-email@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(204) + return cb() + } + ) + }, + cb => { + return this.user.request( + { url: '/user/emails', json: true }, + function(error, response, body) { + expect(response.statusCode).to.equal(200) + expect(body[0].confirmedAt).to.not.exist + expect(body[1].confirmedAt).to.not.exist + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // There should only be one confirmation token at the moment + expect(tokens.length).to.equal(1) + expect(tokens[0].data.email).to.equal( + 'newly-added-email@example.com' + ) + expect(tokens[0].data.user_id).to.equal(this.user._id) + ;({ token } = tokens[0]) + return cb() + } + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/confirm', + json: { + token + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(200) + return cb() + } + ) + }, + cb => { + return this.user.request( + { url: '/user/emails', json: true }, + function(error, response, body) { + expect(response.statusCode).to.equal(200) + expect(body[0].confirmedAt).to.not.exist + expect(body[1].confirmedAt).to.exist + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // Token should be deleted after use + expect(tokens.length).to.equal(0) + return cb() + } + ) + } + ], + done + ) + }) + + return it('should not allow confirmation of the email if the user has changed', function(done) { + let token1 = null + let token2 = null + this.user2 = new User() + this.email = 'duplicate-email@example.com' + return async.series( + [ + cb => this.user2.login(cb), + cb => { + // Create email for first user + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { email: this.email } + }, + cb + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // There should only be one confirmation token at the moment + expect(tokens.length).to.equal(1) + expect(tokens[0].data.email).to.equal(this.email) + expect(tokens[0].data.user_id).to.equal(this.user._id) + token1 = tokens[0].token + return cb() + } + ) + }, + cb => { + // Delete the email from the first user + return this.user.request( + { + method: 'POST', + url: '/user/emails/delete', + json: { email: this.email } + }, + cb + ) + }, + cb => { + // Create email for second user + return this.user2.request( + { + method: 'POST', + url: '/user/emails', + json: { email: this.email } + }, + cb + ) + }, + cb => { + // Original confirmation token should no longer work + return this.user.request( + { + method: 'POST', + url: '/user/emails/confirm', + json: { + token: token1 + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(404) + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user2._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // The first token has been used, so this should be token2 now + expect(tokens.length).to.equal(1) + expect(tokens[0].data.email).to.equal(this.email) + expect(tokens[0].data.user_id).to.equal(this.user2._id) + token2 = tokens[0].token + return cb() + } + ) + }, + cb => { + // Second user should be able to confirm the email + return this.user2.request( + { + method: 'POST', + url: '/user/emails/confirm', + json: { + token: token2 + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(200) + return cb() + } + ) + }, + cb => { + return this.user2.request( + { url: '/user/emails', json: true }, + function(error, response, body) { + expect(response.statusCode).to.equal(200) + expect(body[0].confirmedAt).to.not.exist + expect(body[1].confirmedAt).to.exist + return cb() + } + ) + } + ], + done + ) + }) + }) + + describe('with an expired token', () => + it('should not confirm the email', function(done) { + let token = null + return async.series( + [ + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { + email: (this.email = 'expired-token-email@example.com') + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(204) + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // There should only be one confirmation token at the moment + expect(tokens.length).to.equal(1) + expect(tokens[0].data.email).to.equal(this.email) + expect(tokens[0].data.user_id).to.equal(this.user._id) + ;({ token } = tokens[0]) + return cb() + } + ) + }, + cb => { + return db.tokens.update( + { + token + }, + { + $set: { + expiresAt: new Date(Date.now() - 1000000) + } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/confirm', + json: { + token + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(404) + return cb() + } + ) + } + ], + done + ) + })) + + describe('resending the confirmation', function() { + it('should generate a new token', function(done) { + return async.series( + [ + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { + email: 'reconfirmation-email@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(204) + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // There should only be one confirmation token at the moment + expect(tokens.length).to.equal(1) + expect(tokens[0].data.email).to.equal( + 'reconfirmation-email@example.com' + ) + expect(tokens[0].data.user_id).to.equal(this.user._id) + return cb() + } + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: { + email: 'reconfirmation-email@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(200) + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // There should be two tokens now + expect(tokens.length).to.equal(2) + expect(tokens[0].data.email).to.equal( + 'reconfirmation-email@example.com' + ) + expect(tokens[0].data.user_id).to.equal(this.user._id) + expect(tokens[1].data.email).to.equal( + 'reconfirmation-email@example.com' + ) + expect(tokens[1].data.user_id).to.equal(this.user._id) + return cb() + } + ) + } + ], + done + ) + }) + + it('should create a new token if none exists', function(done) { + // This should only be for users that have sign up with their main + // emails before the confirmation system existed + return async.series( + [ + cb => { + return db.tokens.remove( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: { + email: this.user.email + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(200) + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + // There should still only be one confirmation token + expect(tokens.length).to.equal(1) + expect(tokens[0].data.email).to.equal(this.user.email) + expect(tokens[0].data.user_id).to.equal(this.user._id) + return cb() + } + ) + } + ], + done + ) + }) + + return it("should not allow reconfirmation if the email doesn't match the user", function(done) { + return async.series( + [ + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/resend_confirmation', + json: { + email: 'non-matching-email@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(422) + return cb() + } + ) + }, + cb => { + return db.tokens.find( + { + use: 'email_confirmation', + 'data.user_id': this.user._id, + usedAt: { $exists: false } + }, + (error, tokens) => { + expect(tokens.length).to.equal(0) + return cb() + } + ) + } + ], + done + ) + }) + }) + + return describe('setting a default email', function() { + it('should update confirmed emails for users not in v1', function(done) { + const token = null + return async.series( + [ + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { + email: 'new-confirmed-default@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(204) + return cb() + } + ) + }, + cb => { + // Mark the email as confirmed + return db.users.update( + { + 'emails.email': 'new-confirmed-default@example.com' + }, + { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/default', + json: { + email: 'new-confirmed-default@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(200) + return cb() + } + ) + }, + cb => { + return this.user.request( + { url: '/user/emails', json: true }, + function(error, response, body) { + expect(response.statusCode).to.equal(200) + expect(body[0].confirmedAt).to.not.exist + expect(body[0].default).to.equal(false) + expect(body[1].confirmedAt).to.exist + expect(body[1].default).to.equal(true) + return cb() + } + ) + } + ], + done + ) + }) + + it('should not allow changing unconfirmed emails in v1', function(done) { + const token = null + return async.series( + [ + cb => { + return db.users.update( + { + _id: ObjectId(this.user._id) + }, + { + $set: { + 'overleaf.id': 42 + } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { + email: 'new-unconfirmed-default@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(204) + return cb() + } + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/default', + json: { + email: 'new-unconfirmed-default@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(409) + return cb() + } + ) + }, + cb => { + return this.user.request( + { url: '/user/emails', json: true }, + function(error, response, body) { + expect(body[0].default).to.equal(true) + expect(body[1].default).to.equal(false) + return cb() + } + ) + } + ], + done + ) + }) + + it('should update the email in v1 if confirmed', function(done) { + const token = null + return async.series( + [ + cb => { + return db.users.update( + { + _id: ObjectId(this.user._id) + }, + { + $set: { + 'overleaf.id': 42 + } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { + email: 'new-confirmed-default-in-v1@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(204) + return cb() + } + ) + }, + cb => { + // Mark the email as confirmed + return db.users.update( + { + 'emails.email': 'new-confirmed-default-in-v1@example.com' + }, + { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/default', + json: { + email: 'new-confirmed-default-in-v1@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(200) + return cb() + } + ) + } + ], + error => { + if (error != null) { + return done(error) + } + expect( + MockV1Api.updateEmail.calledWith( + 42, + 'new-confirmed-default-in-v1@example.com' + ) + ).to.equal(true) + return done() + } + ) + }) + + return it('should return an error if the email exists in v1', function(done) { + MockV1Api.existingEmails.push('exists-in-v1@example.com') + return async.series( + [ + cb => { + return db.users.update( + { + _id: ObjectId(this.user._id) + }, + { + $set: { + 'overleaf.id': 42 + } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails', + json: { + email: 'exists-in-v1@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(204) + return cb() + } + ) + }, + cb => { + // Mark the email as confirmed + return db.users.update( + { + 'emails.email': 'exists-in-v1@example.com' + }, + { + $set: { + 'emails.$.confirmedAt': new Date() + } + }, + cb + ) + }, + cb => { + return this.user.request( + { + method: 'POST', + url: '/user/emails/default', + json: { + email: 'exists-in-v1@example.com' + } + }, + (error, response, body) => { + if (error != null) { + return done(error) + } + expect(response.statusCode).to.equal(409) + expect(body).to.deep.equal({ + message: 'This email is already registered' + }) + return cb() + } + ) + }, + cb => { + return this.user.request( + { url: '/user/emails', json: true }, + function(error, response, body) { + expect(body[0].default).to.equal(true) + expect(body[1].default).to.equal(false) + return cb() + } + ) + } + ], + done + ) + }) + }) +}) diff --git a/services/web/test/acceptance/src/UserReconfirmTests.js b/services/web/test/acceptance/src/UserReconfirmTests.js new file mode 100644 index 0000000000..81c6977f35 --- /dev/null +++ b/services/web/test/acceptance/src/UserReconfirmTests.js @@ -0,0 +1,64 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { expect } = require('chai') +const should = require('chai').should() +const async = require('async') +const User = require('./helpers/User') + +describe('User Must Reconfirm', function() { + before(function(done) { + this.user = new User() + return async.series( + [ + this.user.ensureUserExists.bind(this.user), + cb => this.user.mongoUpdate({ $set: { must_reconfirm: true } }, cb) + ], + done + ) + }) + + it('should not allow sign in', function(done) { + return this.user.login(err => { + expect(err != null).to.equal(false) + return this.user.isLoggedIn(function(err, isLoggedIn) { + expect(isLoggedIn).to.equal(false) + return done() + }) + }) + }) + + return describe('Requesting reconfirmation email', function() { + it('should return a success to client for existing account', function(done) { + return this.user.reconfirmAccountRequest( + this.user.email, + (err, response) => { + expect(err != null).to.equal(false) + expect(response.statusCode).to.equal(200) + return done() + } + ) + }) + + return it('should return a 404 to client for non-existent account', function(done) { + return this.user.reconfirmAccountRequest( + 'fake@overleaf.com', + (err, response) => { + expect(err != null).to.equal(false) + expect(response.statusCode).to.equal(404) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/acceptance/src/UserThirdPartyIdentityTests.js b/services/web/test/acceptance/src/UserThirdPartyIdentityTests.js new file mode 100644 index 0000000000..cff22f0204 --- /dev/null +++ b/services/web/test/acceptance/src/UserThirdPartyIdentityTests.js @@ -0,0 +1,206 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Errors = require('../../../app/src/Features/Errors/Errors') +const Settings = require('settings-sharelatex') +const User = require('./helpers/User') +const ThirdPartyIdentityManager = require('../../../app/src/Features/User/ThirdPartyIdentityManager') +const chai = require('chai') + +const { expect } = chai + +describe('ThirdPartyIdentityManager', function() { + beforeEach(function(done) { + this.provider = 'provider' + this.externalUserId = 'external-user-id' + this.externalData = { test: 'data' } + this.user = new User() + return this.user.ensureUserExists(done) + }) + + afterEach(function(done) { + return this.user.full_delete_user(this.user.email, done) + }) + + describe('login', function() { + describe('when third party identity exists', function() { + beforeEach(function(done) { + return ThirdPartyIdentityManager.link( + this.user.id, + this.provider, + this.externalUserId, + this.externalData, + done + ) + }) + + it('should return user', function(done) { + ThirdPartyIdentityManager.login( + this.provider, + this.externalUserId, + this.externalData, + (err, user) => { + expect(err).to.be.null + expect(user._id.toString()).to.equal(this.user.id) + return done() + } + ) + }) + + return it('should merge external data', function(done) { + this.externalData = { + test: 'different', + another: 'key' + } + ThirdPartyIdentityManager.login( + this.provider, + this.externalUserId, + this.externalData, + (err, user) => { + expect(err).to.be.null + expect(user.thirdPartyIdentifiers[0].externalData).to.deep.equal( + this.externalData + ) + return done() + } + ) + }) + }) + + return describe('when third party identity does not exists', () => + it('should return error', function(done) { + ThirdPartyIdentityManager.login( + this.provider, + this.externalUserId, + this.externalData, + (err, user) => { + expect(err.name).to.equal('ThirdPartyUserNotFoundError') + return done() + } + ) + })) + }) + + describe('link', function() { + describe('when provider not already linked', () => + it('should link provider to user', function(done) { + return ThirdPartyIdentityManager.link( + this.user.id, + this.provider, + this.externalUserId, + this.externalData, + function(err, res) { + expect(res.nModified).to.equal(1) + return done() + } + ) + })) + + return describe('when provider is already linked', function() { + beforeEach(function(done) { + return ThirdPartyIdentityManager.link( + this.user.id, + this.provider, + this.externalUserId, + this.externalData, + done + ) + }) + + it('should link provider to user', function(done) { + return ThirdPartyIdentityManager.link( + this.user.id, + this.provider, + this.externalUserId, + this.externalData, + function(err, res) { + expect(res.nModified).to.equal(1) + return done() + } + ) + }) + + it('should not create duplicate thirdPartyIdentifiers', function(done) { + return ThirdPartyIdentityManager.link( + this.user.id, + this.provider, + this.externalUserId, + this.externalData, + (err, res) => { + return this.user.get(function(err, user) { + expect(user.thirdPartyIdentifiers.length).to.equal(1) + return done() + }) + } + ) + }) + + return it('should replace existing data', function(done) { + this.externalData = { replace: 'data' } + return ThirdPartyIdentityManager.link( + this.user.id, + this.provider, + this.externalUserId, + this.externalData, + (err, res) => { + return this.user.get((err, user) => { + expect(user.thirdPartyIdentifiers[0].externalData).to.deep.equal( + this.externalData + ) + return done() + }) + } + ) + }) + }) + }) + + return describe('unlink', function() { + describe('when provider not already linked', () => + it('should succeed', function(done) { + return ThirdPartyIdentityManager.unlink( + this.user.id, + this.provider, + function(err, res) { + expect(err).to.be.null + expect(res.nModified).to.equal(0) + return done() + } + ) + })) + + return describe('when provider is already linked', function() { + beforeEach(function(done) { + return ThirdPartyIdentityManager.link( + this.user.id, + this.provider, + this.externalUserId, + this.externalData, + done + ) + }) + + return it('should remove thirdPartyIdentifiers entry', function(done) { + return ThirdPartyIdentityManager.unlink( + this.user.id, + this.provider, + (err, res) => { + return this.user.get(function(err, user) { + expect(user.thirdPartyIdentifiers.length).to.equal(0) + return done() + }) + } + ) + }) + }) + }) +}) diff --git a/services/web/test/acceptance/src/helpers/MockClsiApi.js b/services/web/test/acceptance/src/helpers/MockClsiApi.js new file mode 100644 index 0000000000..5f04b0aa37 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockClsiApi.js @@ -0,0 +1,89 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockClsiApi +const express = require('express') +const bodyParser = require('body-parser') +const app = express() + +module.exports = MockClsiApi = { + run() { + const compile = (req, res, next) => { + return res.status(200).send({ + compile: { + status: 'success', + error: null, + outputFiles: [ + { + url: `/project/${ + req.params.project_id + }/build/1234/output/project.pdf`, + path: 'project.pdf', + type: 'pdf', + build: 1234 + }, + { + url: `/project/${ + req.params.project_id + }/build/1234/output/project.log`, + path: 'project.log', + type: 'log', + build: 1234 + } + ] + } + }) + } + + app.post('/project/:project_id/compile', compile) + app.post('/project/:project_id/user/:user_id/compile', compile) + + app.get('/project/:project_id/build/:build_id/output/*', function( + req, + res, + next + ) { + const filename = req.params[0] + if (filename === 'project.pdf') { + return res.status(200).send('mock-pdf') + } else if (filename === 'project.log') { + return res.status(200).send('mock-log') + } else { + return res.sendStatus(404) + } + }) + + app.get( + '/project/:project_id/user/:user_id/build/:build_id/output/:output_path', + (req, res, next) => { + return res.status(200).send('hello') + } + ) + + app.get('/project/:project_id/status', (req, res, next) => { + return res.status(200).send() + }) + + return app + .listen(3013, function(error) { + if (error != null) { + throw error + } + }) + .on('error', function(error) { + console.error('error starting MockClsiApi:', error.message) + return process.exit(1) + }) + } +} + +MockClsiApi.run() diff --git a/services/web/test/acceptance/src/helpers/MockDocUpdaterApi.js b/services/web/test/acceptance/src/helpers/MockDocUpdaterApi.js new file mode 100644 index 0000000000..1e72746088 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockDocUpdaterApi.js @@ -0,0 +1,108 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockDocUpdaterApi +const express = require('express') +const app = express() +const bodyParser = require('body-parser') +const jsonParser = bodyParser.json() + +module.exports = MockDocUpdaterApi = { + updates: {}, + + clearProjectStructureUpdates() { + return (this.updates = {}) + }, + + getProjectStructureUpdates(project_id) { + return this.updates[project_id] || { docUpdates: [], fileUpdates: [] } + }, + + addProjectStructureUpdates( + project_id, + userId, + docUpdates, + fileUpdates, + version + ) { + let update + if (!this.updates[project_id]) { + this.updates[project_id] = { docUpdates: [], fileUpdates: [] } + } + + for (update of Array.from(docUpdates)) { + update.userId = userId + this.updates[project_id].docUpdates.push(update) + } + + for (update of Array.from(fileUpdates)) { + update.userId = userId + this.updates[project_id].fileUpdates.push(update) + } + + return (this.updates[project_id].version = version) + }, + + run() { + app.post('/project/:project_id/flush', (req, res, next) => { + return res.sendStatus(204) + }) + + app.post('/project/:project_id', jsonParser, (req, res, next) => { + const { project_id } = req.params + const { userId, docUpdates, fileUpdates, version } = req.body + this.addProjectStructureUpdates( + project_id, + userId, + docUpdates, + fileUpdates, + version + ) + return res.sendStatus(200) + }) + + app.post('/project/:project_id/doc/:doc_id', (req, res, next) => { + return res.sendStatus(204) + }) + + app.delete('/project/:project_id', (req, res) => { + return res.sendStatus(204) + }) + + app.post('/project/:project_id/doc/:doc_id/flush', (req, res, next) => { + return res.sendStatus(204) + }) + + app.delete('/project/:project_id/doc/:doc_id', (req, res, next) => { + return res.sendStatus(204) + }) + + app.post('/project/:project_id/history/resync', (req, res, next) => { + return res.sendStatus(204) + }) + + return app + .listen(3003, function(error) { + if (error != null) { + throw error + } + }) + .on('error', function(error) { + console.error('error starting MockDocUpdaterApi:', error.message) + return process.exit(1) + }) + } +} + +MockDocUpdaterApi.run() diff --git a/services/web/test/acceptance/src/helpers/MockDocstoreApi.js b/services/web/test/acceptance/src/helpers/MockDocstoreApi.js new file mode 100644 index 0000000000..e6213ae6b8 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockDocstoreApi.js @@ -0,0 +1,92 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockDocStoreApi +const express = require('express') +const bodyParser = require('body-parser') +const app = express() + +module.exports = MockDocStoreApi = { + docs: {}, + + run() { + app.post( + '/project/:project_id/doc/:doc_id', + bodyParser.json(), + (req, res, next) => { + const { project_id, doc_id } = req.params + const { lines, version, ranges } = req.body + if (this.docs[project_id] == null) { + this.docs[project_id] = {} + } + this.docs[project_id][doc_id] = { lines, version, ranges } + if (this.docs[project_id][doc_id].rev == null) { + this.docs[project_id][doc_id].rev = 0 + } + this.docs[project_id][doc_id].rev += 1 + this.docs[project_id][doc_id]._id = doc_id + return res.json({ + modified: true, + rev: this.docs[project_id][doc_id].rev + }) + } + ) + + app.get('/project/:project_id/doc', (req, res, next) => { + const docs = (() => { + const result = [] + for (let doc_id in this.docs[req.params.project_id]) { + const doc = this.docs[req.params.project_id][doc_id] + result.push(doc) + } + return result + })() + return res.json(docs) + }) + + app.get('/project/:project_id/doc/:doc_id', (req, res, next) => { + const { project_id, doc_id } = req.params + const doc = this.docs[project_id][doc_id] + if (doc == null || (doc.deleted && !req.query.include_deleted)) { + return res.sendStatus(404) + } else { + return res.json(doc) + } + }) + + app.delete('/project/:project_id/doc/:doc_id', (req, res, next) => { + const { project_id, doc_id } = req.params + if (this.docs[project_id] == null) { + return res.sendStatus(404) + } else if (this.docs[project_id][doc_id] == null) { + return res.sendStatus(404) + } else { + this.docs[project_id][doc_id].deleted = true + return res.sendStatus(204) + } + }) + + return app + .listen(3016, function(error) { + if (error != null) { + throw error + } + }) + .on('error', function(error) { + console.error('error starting MockDocStoreApi:', error.message) + return process.exit(1) + }) + } +} + +MockDocStoreApi.run() diff --git a/services/web/test/acceptance/src/helpers/MockFileStoreApi.js b/services/web/test/acceptance/src/helpers/MockFileStoreApi.js new file mode 100644 index 0000000000..4ae9216035 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockFileStoreApi.js @@ -0,0 +1,79 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockFileStoreApi +const express = require('express') +const bodyParser = require('body-parser') +const app = express() + +module.exports = MockFileStoreApi = { + files: {}, + + run() { + app.post('/project/:project_id/file/:file_id', (req, res, next) => { + const chunks = [] + req.on('data', chunk => chunks.push(chunk)) + + return req.on('end', () => { + const content = Buffer.concat(chunks).toString() + const { project_id, file_id } = req.params + if (this.files[project_id] == null) { + this.files[project_id] = {} + } + this.files[project_id][file_id] = { content } + return res.sendStatus(200) + }) + }) + + app.get('/project/:project_id/file/:file_id', (req, res, next) => { + const { project_id, file_id } = req.params + const { content } = this.files[project_id][file_id] + return res.send(content) + }) + + // handle file copying + app.put( + '/project/:project_id/file/:file_id', + bodyParser.json(), + (req, res, next) => { + const { project_id, file_id } = req.params + const { source } = req.body + const { content } = + this.files[source.project_id] != null + ? this.files[source.project_id][source.file_id] + : undefined + if (content == null) { + return res.sendStatus(500) + } else { + if (this.files[project_id] == null) { + this.files[project_id] = {} + } + this.files[project_id][file_id] = { content } + return res.sendStatus(200) + } + } + ) + + return app + .listen(3009, function(error) { + if (error != null) { + throw error + } + }) + .on('error', function(error) { + console.error('error starting MockFileStoreApi:', error.message) + return process.exit(1) + }) + } +} + +MockFileStoreApi.run() diff --git a/services/web/test/acceptance/src/helpers/MockProjectHistoryApi.js b/services/web/test/acceptance/src/helpers/MockProjectHistoryApi.js new file mode 100644 index 0000000000..f05dc4c4f6 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockProjectHistoryApi.js @@ -0,0 +1,168 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockProjectHistoryApi +const _ = require('lodash') +const express = require('express') +const bodyParser = require('body-parser') +const app = express() +const { ObjectId } = require('mongojs') + +module.exports = MockProjectHistoryApi = { + docs: {}, + + oldFiles: {}, + + projectVersions: {}, + + labels: {}, + + projectSnapshots: {}, + + addOldFile(project_id, version, pathname, content) { + return (this.oldFiles[`${project_id}:${version}:${pathname}`] = content) + }, + + addProjectSnapshot(project_id, version, snapshot) { + return (this.projectSnapshots[`${project_id}:${version}`] = snapshot) + }, + + setProjectVersion(project_id, version) { + return (this.projectVersions[project_id] = { version }) + }, + + setProjectVersionInfo(project_id, versionInfo) { + return (this.projectVersions[project_id] = versionInfo) + }, + + addLabel(project_id, label) { + if (label.id == null) { + label.id = new ObjectId().toString() + } + if (this.labels[project_id] == null) { + this.labels[project_id] = {} + } + return (this.labels[project_id][label.id] = label) + }, + + deleteLabel(project_id, label_id) { + return delete this.labels[project_id][label_id] + }, + + getLabels(project_id) { + if (this.labels[project_id] == null) { + return null + } + return _.values(this.labels[project_id]) + }, + + reset() { + this.oldFiles = {} + this.projectVersions = {} + return (this.labels = {}) + }, + + run() { + app.post('/project', (req, res, next) => { + return res.json({ project: { id: 1 } }) + }) + + app.get( + '/project/:project_id/version/:version/:pathname', + (req, res, next) => { + const { project_id, version, pathname } = req.params + const key = `${project_id}:${version}:${pathname}` + if (this.oldFiles[key] != null) { + return res.send(this.oldFiles[key]) + } else { + return res.send(404) + } + } + ) + + app.get('/project/:project_id/version/:version', (req, res, next) => { + const { project_id, version } = req.params + const key = `${project_id}:${version}` + if (this.projectSnapshots[key] != null) { + return res.json(this.projectSnapshots[key]) + } else { + return res.sendStatus(404) + } + }) + + app.get('/project/:project_id/version', (req, res, next) => { + const { project_id } = req.params + if (this.projectVersions[project_id] != null) { + return res.json(this.projectVersions[project_id]) + } else { + return res.send(404) + } + }) + + app.get('/project/:project_id/labels', (req, res, next) => { + const { project_id } = req.params + const labels = this.getLabels(project_id) + if (labels != null) { + return res.json(labels) + } else { + return res.send(404) + } + }) + + app.post( + '/project/:project_id/user/:user_id/labels', + bodyParser.json(), + (req, res, next) => { + const { project_id } = req.params + const { comment, version } = req.body + const label_id = new ObjectId().toString() + this.addLabel(project_id, { id: label_id, comment, version }) + return res.json({ label_id, comment, version }) + } + ) + + app.delete( + '/project/:project_id/user/:user_id/labels/:label_id', + (req, res, next) => { + const { project_id, label_id } = req.params + const label = + this.labels[project_id] != null + ? this.labels[project_id][label_id] + : undefined + if (label != null) { + this.deleteLabel(project_id, label_id) + return res.send(204) + } else { + return res.send(404) + } + } + ) + + app.post('/project/:project_id/flush', (req, res, next) => { + return res.sendStatus(200) + }) + + return app + .listen(3054, function(error) { + if (error != null) { + throw error + } + }) + .on('error', function(error) { + console.error('error starting MockProjectHistoryApi:', error.message) + return process.exit(1) + }) + } +} + +MockProjectHistoryApi.run() diff --git a/services/web/test/acceptance/src/helpers/MockRecurlyApi.js b/services/web/test/acceptance/src/helpers/MockRecurlyApi.js new file mode 100644 index 0000000000..4fa9e6a193 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockRecurlyApi.js @@ -0,0 +1,111 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockRecurlyApi +const express = require('express') +const app = express() +const bodyParser = require('body-parser') + +app.use(bodyParser.json()) + +module.exports = MockRecurlyApi = { + subscriptions: {}, + + accounts: {}, + + redemptions: {}, + + coupons: {}, + + run() { + app.get('/subscriptions/:id', (req, res, next) => { + const subscription = this.subscriptions[req.params.id] + if (subscription == null) { + return res.status(404).end() + } else { + return res.send(`\ + + ${subscription.plan_code} + ${subscription.currency} + ${subscription.state} + ${subscription.tax_in_cents} + ${subscription.tax_rate} + ${ + subscription.current_period_ends_at + } + ${ + subscription.unit_amount_in_cents + } + + ${subscription.trial_ends_at} +\ +`) + } + }) + + app.get('/accounts/:id', (req, res, next) => { + const account = this.accounts[req.params.id] + if (account == null) { + return res.status(404).end() + } else { + return res.send(`\ + + ${req.params.id} + ${account.hosted_login_token} +\ +`) + } + }) + + app.get('/coupons/:code', (req, res, next) => { + const coupon = this.coupons[req.params.code] + if (coupon == null) { + return res.status(404).end() + } else { + return res.send(`\ + + ${req.params.code} + ${coupon.name || ''} + ${coupon.description || ''} +\ +`) + } + }) + + app.get('/accounts/:id/redemptions', (req, res, next) => { + const redemptions = this.redemptions[req.params.id] || [] + let redemptionsListXml = '' + for (let redemption of Array.from(redemptions)) { + redemptionsListXml += `\ + + ${redemption.state} + ${redemption.coupon_code} +\ +` + } + + return res.send(`\ + + ${redemptionsListXml} +\ +`) + }) + + return app.listen(6034, function(error) { + if (error != null) { + throw error + } + }) + } +} + +MockRecurlyApi.run() diff --git a/services/web/test/acceptance/src/helpers/MockV1Api.js b/services/web/test/acceptance/src/helpers/MockV1Api.js new file mode 100644 index 0000000000..e14dbd3865 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockV1Api.js @@ -0,0 +1,277 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockV1Api +const express = require('express') +const app = express() +const bodyParser = require('body-parser') +const sinon = require('sinon') + +app.use(bodyParser.json()) + +let v1Id = 1000 + +module.exports = MockV1Api = { + nextV1Id() { + return v1Id++ + }, + + users: {}, + + setUser(id, user) { + return (this.users[id] = user) + }, + + exportId: null, + + exportParams: null, + + setExportId(id) { + return (this.exportId = id) + }, + + getLastExportParams() { + return this.exportParams + }, + + clearExportParams() { + return (this.exportParams = null) + }, + + syncUserFeatures: sinon.stub(), + + affiliations: [], + + updateEmail: sinon.stub(), + + existingEmails: [], + + brands: {}, + + brand_variations: {}, + + validation_clients: {}, + + setAffiliations(affiliations) { + return (this.affiliations = affiliations) + }, + + doc_exported: {}, + + setDocExported(token, info) { + return (this.doc_exported[token] = info) + }, + + run() { + app.get( + '/api/v1/sharelatex/users/:v1_user_id/plan_code', + (req, res, next) => { + const user = this.users[req.params.v1_user_id] + if (user) { + return res.json(user) + } else { + return res.sendStatus(404) + } + } + ) + + app.get( + '/api/v1/sharelatex/users/:v1_user_id/subscriptions', + (req, res, next) => { + const user = this.users[req.params.v1_user_id] + if ((user != null ? user.subscription : undefined) != null) { + return res.json(user.subscription) + } else { + return res.sendStatus(404) + } + } + ) + + app.get( + '/api/v1/sharelatex/users/:v1_user_id/subscription_status', + (req, res, next) => { + const user = this.users[req.params.v1_user_id] + if ((user != null ? user.subscription_status : undefined) != null) { + return res.json(user.subscription_status) + } else { + return res.sendStatus(404) + } + } + ) + + app.delete( + '/api/v1/sharelatex/users/:v1_user_id/subscription', + (req, res, next) => { + const user = this.users[req.params.v1_user_id] + if (user != null) { + user.canceled = true + return res.sendStatus(200) + } else { + return res.sendStatus(404) + } + } + ) + + app.post('/api/v1/sharelatex/users/:v1_user_id/sync', (req, res, next) => { + this.syncUserFeatures(req.params.v1_user_id) + return res.sendStatus(200) + }) + + app.post('/api/v1/sharelatex/exports', (req, res, next) => { + this.exportParams = Object.assign({}, req.body) + return res.json({ exportId: this.exportId }) + }) + + app.get('/api/v2/users/:userId/affiliations', (req, res, next) => { + return res.json(this.affiliations) + }) + + app.post('/api/v2/users/:userId/affiliations', (req, res, next) => { + return res.sendStatus(201) + }) + + app.delete( + '/api/v2/users/:userId/affiliations/:email', + (req, res, next) => { + return res.sendStatus(204) + } + ) + + app.get('/api/v2/brands/:slug', (req, res, next) => { + let brand + if ((brand = this.brands[req.params.slug])) { + return res.json(brand) + } else { + return res.sendStatus(404) + } + }) + + app.get('/universities/list', (req, res, next) => res.json([])) + + app.get('/universities/list/:id', (req, res, next) => + res.json({ + id: parseInt(req.params.id), + name: `Institution ${req.params.id}` + }) + ) + + app.get('/university/domains', (req, res, next) => res.json([])) + + app.put('/api/v1/sharelatex/users/:id/email', (req, res, next) => { + const { email } = req.body != null ? req.body.user : undefined + if (Array.from(this.existingEmails).includes(email)) { + return res.sendStatus(409) + } else { + this.updateEmail(parseInt(req.params.id), email) + return res.sendStatus(200) + } + }) + + app.post('/api/v1/sharelatex/login', (req, res, next) => { + for (let id in this.users) { + const user = this.users[id] + if ( + user != null && + user.email === req.body.email && + user.password === req.body.password + ) { + return res.json({ + email: user.email, + valid: true, + user_profile: user.profile + }) + } + } + return res.status(403).json({ + email: req.body.email, + valid: false + }) + }) + + app.get('/api/v2/partners/:partner/conversions/:id', (req, res, next) => { + const partner = this.validation_clients[req.params.partner] + const conversion = __guard__( + partner != null ? partner.conversions : undefined, + x => x[req.params.id] + ) + if (conversion != null) { + return res.status(200).json({ + input_file_uri: conversion, + brand_variation_id: partner.brand_variation_id + }) + } else { + return res.status(404).json({}) + } + }) + + app.get('/api/v2/brand_variations/:id', (req, res, next) => { + const variation = this.brand_variations[req.params.id] + if (variation != null) { + return res.status(200).json(variation) + } else { + return res.status(404).json({}) + } + }) + + app.get('/api/v1/sharelatex/docs/:token/is_published', (req, res, next) => { + return res.json({ allow: true }) + }) + + app.get( + '/api/v1/sharelatex/users/:user_id/docs/:token/info', + (req, res, next) => { + return res.json({ + exists: true, + exported: false + }) + } + ) + + app.get( + '/api/v1/sharelatex/docs/:token/exported_to_v2', + (req, res, next) => { + if (this.doc_exported[req.params.token] != null) { + return res.json(this.doc_exported[req.params.token]) + } + return res.json({ exporting: false, exported: false }) + } + ) + + app.get( + '/api/v1/sharelatex/docs/read_token/:token/exists', + (req, res, next) => { + return res.json({ exists: false }) + } + ) + + return app + .listen(5000, function(error) { + if (error != null) { + throw error + } + }) + .on('error', function(error) { + console.error('error starting MockV1Api:', error.message) + return process.exit(1) + }) + } +} + +MockV1Api.run() + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/test/acceptance/src/helpers/MockV1HistoryApi.js b/services/web/test/acceptance/src/helpers/MockV1HistoryApi.js new file mode 100644 index 0000000000..b7bc3ee2dc --- /dev/null +++ b/services/web/test/acceptance/src/helpers/MockV1HistoryApi.js @@ -0,0 +1,76 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockV1HistoryApi +const _ = require('lodash') +const express = require('express') +const bodyParser = require('body-parser') +const app = express() +const { ObjectId } = require('mongojs') + +module.exports = MockV1HistoryApi = { + fakeZipCall: 0, + run() { + app.get( + '/api/projects/:project_id/version/:version/zip', + (req, res, next) => { + res.header('content-disposition', 'attachment; name=project.zip') + res.header('content-type', 'application/octet-stream') + return res.send( + `Mock zip for ${req.params.project_id} at version ${ + req.params.version + }` + ) + } + ) + + app.get( + '/fake-zip-download/:project_id/version/:version', + (req, res, next) => { + if (!(this.fakeZipCall++ > 0)) { + return res.sendStatus(404) + } + res.header('content-disposition', 'attachment; name=project.zip') + res.header('content-type', 'application/octet-stream') + return res.send( + `Mock zip for ${req.params.project_id} at version ${ + req.params.version + }` + ) + } + ) + + app.post( + '/api/projects/:project_id/version/:version/zip', + (req, res, next) => { + return res.json({ + zipUrl: `http://localhost:3100/fake-zip-download/${ + req.params.project_id + }/version/${req.params.version}` + }) + } + ) + + return app + .listen(3100, function(error) { + if (error != null) { + throw error + } + }) + .on('error', function(error) { + console.error('error starting MockV1HistoryApi:', error.message) + return process.exit(1) + }) + } +} + +MockV1HistoryApi.run() diff --git a/services/web/test/acceptance/src/helpers/User.js b/services/web/test/acceptance/src/helpers/User.js new file mode 100644 index 0000000000..7c01ccb716 --- /dev/null +++ b/services/web/test/acceptance/src/helpers/User.js @@ -0,0 +1,751 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-undef, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const request = require('./request') +const _ = require('underscore') +const settings = require('settings-sharelatex') +const { db, ObjectId } = require('../../../../app/src/infrastructure/mongojs') +const UserModel = require('../../../../app/src/models/User').User +const UserUpdater = require('../../../../app/src/Features/User/UserUpdater') +const AuthenticationManager = require('../../../../app/src/Features/Authentication/AuthenticationManager') + +let count = 0 + +class User { + constructor(options) { + if (options == null) { + options = {} + } + this.emails = [ + { + email: options.email || `acceptance-test-${count}@example.com`, + createdAt: new Date() + } + ] + this.email = this.emails[0].email + this.password = `acceptance-test-${count}-password` + count++ + this.jar = request.jar() + this.request = request.defaults({ + jar: this.jar + }) + } + + setExtraAttributes(user) { + if ((user != null ? user._id : undefined) == null) { + throw new Error('User does not exist') + } + this.id = user._id.toString() + this._id = user._id.toString() + this.first_name = user.first_name + return (this.referal_id = user.referal_id) + } + + get(callback) { + if (callback == null) { + callback = function(error, user) {} + } + return db.users.findOne({ _id: ObjectId(this._id) }, callback) + } + + mongoUpdate(updateOp, callback) { + if (callback == null) { + callback = function(error) {} + } + return db.users.update({ _id: ObjectId(this._id) }, updateOp, callback) + } + + register(callback) { + if (callback == null) { + callback = function(error, user) {} + } + return this.registerWithQuery('', callback) + } + + registerWithQuery(query, callback) { + if (callback == null) { + callback = function(error, user) {} + } + if (this._id != null) { + return callback(new Error('User already registered')) + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + url: `/register${query}`, + json: { email: this.email, password: this.password } + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return db.users.findOne({ email: this.email }, (error, user) => { + if (error != null) { + return callback(error) + } + this.setExtraAttributes(user) + return callback(null, user) + }) + } + ) + }) + } + + login(callback) { + if (callback == null) { + callback = function(error) {} + } + return this.loginWith(this.email, callback) + } + + loginWith(email, callback) { + if (callback == null) { + callback = function(error) {} + } + return this.ensureUserExists(error => { + if (error != null) { + return callback(error) + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + url: settings.enableLegacyLogin ? '/login/legacy' : '/login', + json: { email, password: this.password } + }, + callback + ) + }) + }) + } + + ensureUserExists(callback) { + if (callback == null) { + callback = function(error) {} + } + const filter = { email: this.email } + const options = { upsert: true, new: true, setDefaultsOnInsert: true } + return UserModel.findOneAndUpdate(filter, {}, options, (error, user) => { + if (error != null) { + return callback(error) + } + return AuthenticationManager.setUserPasswordInV2( + user._id, + this.password, + error => { + if (error != null) { + return callback(error) + } + return UserUpdater.updateUser( + user._id, + { $set: { emails: this.emails } }, + error => { + if (error != null) { + return callback(error) + } + this.setExtraAttributes(user) + return callback(null, this.password) + } + ) + } + ) + }) + } + + setFeatures(features, callback) { + if (callback == null) { + callback = function(error) {} + } + const update = {} + for (let key in features) { + const value = features[key] + update[`features.${key}`] = value + } + return UserModel.update({ _id: this.id }, update, callback) + } + + setOverleafId(overleaf_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return UserModel.update( + { _id: this.id }, + { 'overleaf.id': overleaf_id }, + callback + ) + } + + logout(callback) { + if (callback == null) { + callback = function(error) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + url: '/logout', + json: { + email: this.email, + password: this.password + } + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return db.users.findOne({ email: this.email }, (error, user) => { + if (error != null) { + return callback(error) + } + this.id = __guard__(user != null ? user._id : undefined, x => + x.toString() + ) + this._id = __guard__(user != null ? user._id : undefined, x1 => + x1.toString() + ) + return callback() + }) + } + ) + }) + } + + addEmail(email, callback) { + if (callback == null) { + callback = function(error) {} + } + this.emails.push({ email, createdAt: new Date() }) + return UserUpdater.addEmailAddress(this.id, email, callback) + } + + confirmEmail(email, callback) { + if (callback == null) { + callback = function(error) {} + } + for (let idx = 0; idx < this.emails.length; idx++) { + const emailData = this.emails[idx] + if (emailData.email === email) { + this.emails[idx].confirmedAt = new Date() + } + } + return UserUpdater.confirmEmail(this.id, email, callback) + } + + ensure_admin(callback) { + if (callback == null) { + callback = function(error) {} + } + return db.users.update( + { _id: ObjectId(this.id) }, + { $set: { isAdmin: true } }, + callback + ) + } + + upgradeFeatures(callback) { + if (callback == null) { + callback = function(error) {} + } + const features = { + collaborators: -1, // Infinite + versioning: true, + dropbox: true, + compileTimeout: 60, + compileGroup: 'priority', + templates: true, + references: true, + trackChanges: true, + trackChangesVisible: true + } + return db.users.update( + { _id: ObjectId(this.id) }, + { $set: { features } }, + callback + ) + } + + downgradeFeatures(callback) { + if (callback == null) { + callback = function(error) {} + } + const features = { + collaborators: 1, + versioning: false, + dropbox: false, + compileTimeout: 60, + compileGroup: 'standard', + templates: false, + references: false, + trackChanges: false, + trackChangesVisible: false + } + return db.users.update( + { _id: ObjectId(this.id) }, + { $set: { features } }, + callback + ) + } + + defaultFeatures(callback) { + if (callback == null) { + callback = function(error) {} + } + const features = settings.defaultFeatures + return db.users.update( + { _id: ObjectId(this.id) }, + { $set: { features } }, + callback + ) + } + + full_delete_user(email, callback) { + if (callback == null) { + callback = function(error) {} + } + return db.users.findOne({ email }, (error, user) => { + if (user == null) { + return callback() + } + const user_id = user._id + return db.projects.remove( + { owner_ref: ObjectId(user_id) }, + { multi: true }, + function(err) { + if (err != null) { + callback(err) + } + return db.users.remove({ _id: ObjectId(user_id) }, callback) + } + ) + }) + } + + getProject(project_id, callback) { + if (callback == null) { + callback = function(error, project) {} + } + return db.projects.findOne( + { _id: ObjectId(project_id.toString()) }, + callback + ) + } + + saveProject(project, callback) { + if (callback == null) { + callback = function(error) {} + } + return db.projects.update({ _id: project._id }, project, callback) + } + + createProject(name, options, callback) { + if (callback == null) { + callback = function(error, oroject_id) {} + } + if (typeof options === 'function') { + callback = options + options = {} + } + + return this.request.post( + { + url: '/project/new', + json: Object.assign({ projectName: name }, options) + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + if ((body != null ? body.project_id : undefined) == null) { + error = new Error( + 'SOMETHING WENT WRONG CREATING PROJECT', + response.statusCode, + response.headers['location'], + body + ) + return callback(error) + } else { + return callback(null, body.project_id) + } + } + ) + } + + deleteProject(project_id, callback) { + if (callback == null) { + callback = error + } + return this.request.delete( + { + url: `/project/${project_id}` + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + return callback(null) + } + ) + } + + deleteProjects(callback) { + if (callback == null) { + callback = error + } + return db.projects.remove( + { owner_ref: ObjectId(this.id) }, + { multi: true }, + err => callback(err) + ) + } + + openProject(project_id, callback) { + if (callback == null) { + callback = error + } + return this.request.get( + { + url: `/project/${project_id}` + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + if (response.statusCode !== 200) { + const err = new Error( + `Non-success response when opening project: ${response.statusCode}` + ) + return callback(err) + } + return callback(null) + } + ) + } + + createDocInProject(project_id, parent_folder_id, name, callback) { + if (callback == null) { + callback = function(error, doc_id) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + url: `/project/${project_id}/doc`, + json: { + name, + parent_folder_id + } + }, + (error, response, body) => { + return callback(null, body._id) + } + ) + }) + } + + addUserToProject(project_id, user, privileges, callback) { + let updateOp + if (callback == null) { + callback = function(error, user) {} + } + if (privileges === 'readAndWrite') { + updateOp = { $addToSet: { collaberator_refs: user._id.toString() } } + } else if (privileges === 'readOnly') { + updateOp = { $addToSet: { readOnly_refs: user._id.toString() } } + } + return db.projects.update({ _id: db.ObjectId(project_id) }, updateOp, err => + callback(err) + ) + } + + makePublic(project_id, level, callback) { + if (callback == null) { + callback = function(error) {} + } + return this.request.post( + { + url: `/project/${project_id}/settings/admin`, + json: { + publicAccessLevel: level + } + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + return callback(null) + } + ) + } + + makePrivate(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return this.request.post( + { + url: `/project/${project_id}/settings/admin`, + json: { + publicAccessLevel: 'private' + } + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + return callback(null) + } + ) + } + + makeTokenBased(project_id, callback) { + if (callback == null) { + callback = function(error) {} + } + return this.request.post( + { + url: `/project/${project_id}/settings/admin`, + json: { + publicAccessLevel: 'tokenBased' + } + }, + function(error, response, body) { + if (error != null) { + return callback(error) + } + return callback(null) + } + ) + } + + getCsrfToken(callback) { + if (callback == null) { + callback = function(error) {} + } + return this.request.get( + { + url: '/dev/csrf' + }, + (err, response, body) => { + if (err != null) { + return callback(err) + } + this.csrfToken = body + this.request = this.request.defaults({ + headers: { + 'x-csrf-token': this.csrfToken + } + }) + return callback() + } + ) + } + + changePassword(callback) { + if (callback == null) { + callback = function(error) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + url: '/user/password/update', + json: { + currentPassword: this.password, + newPassword1: this.password, + newPassword2: this.password + } + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return db.users.findOne({ email: this.email }, (error, user) => { + if (error != null) { + return callback(error) + } + return callback() + }) + } + ) + }) + } + + reconfirmAccountRequest(user_email, callback) { + if (callback == null) { + callback = function(error) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + url: '/user/reconfirm', + json: { + email: user_email + } + }, + (error, response, body) => { + return callback(error, response) + } + ) + }) + } + + getUserSettingsPage(callback) { + if (callback == null) { + callback = function(error, statusCode) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.get( + { + url: '/user/settings' + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return callback(null, response.statusCode) + } + ) + }) + } + + activateSudoMode(callback) { + if (callback == null) { + callback = function(error) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + uri: '/confirm-password', + json: { + password: this.password + } + }, + callback + ) + }) + } + + updateSettings(newSettings, callback) { + if (callback == null) { + callback = function(error, response, body) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.post( + { + url: '/user/settings', + json: newSettings + }, + callback + ) + }) + } + + getProjectListPage(callback) { + if (callback == null) { + callback = function(error, statusCode) {} + } + return this.getCsrfToken(error => { + if (error != null) { + return callback(error) + } + return this.request.get( + { + url: '/project' + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return callback(null, response.statusCode) + } + ) + }) + } + + isLoggedIn(callback) { + if (callback == null) { + callback = function(error, loggedIn) {} + } + return this.request.get('/user/personal_info', function( + error, + response, + body + ) { + if (error != null) { + return callback(error) + } + if (response.statusCode === 200) { + return callback(null, true) + } else if (response.statusCode === 302) { + return callback(null, false) + } else { + return callback( + new Error( + `unexpected status code from /user/personal_info: ${ + response.statusCode + }` + ) + ) + } + }) + } + + setV1Id(v1Id, callback) { + return UserModel.update( + { + _id: this._id + }, + { + overleaf: { + id: v1Id + } + }, + callback + ) + } +} + +module.exports = User + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/test/acceptance/src/helpers/redis.js b/services/web/test/acceptance/src/helpers/redis.js new file mode 100644 index 0000000000..2afb8334bc --- /dev/null +++ b/services/web/test/acceptance/src/helpers/redis.js @@ -0,0 +1,57 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const Async = require('async') + +const UserSessionsRedis = require('../../../../app/src/Features/User/UserSessionsRedis') + +// rclient = redis.createClient(Settings.redis.web) +const rclient = UserSessionsRedis.client() + +module.exports = { + getUserSessions(user, callback) { + if (callback == null) { + callback = function(err, sessionsSet) {} + } + return rclient.smembers( + UserSessionsRedis.sessionSetKey(user), + (err, result) => callback(err, result) + ) + }, + + clearUserSessions(user, callback) { + if (callback == null) { + callback = function(err) {} + } + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + return rclient.smembers(sessionSetKey, function(err, sessionKeys) { + if (err) { + return callback(err) + } + if (sessionKeys.length === 0) { + return callback(null) + } + const actions = sessionKeys.map(k => cb => rclient.del(k, err => cb(err))) + return Async.series(actions, (err, results) => + rclient.srem(sessionSetKey, sessionKeys, function(err) { + if (err) { + return callback(err) + } + return callback(null) + }) + ) + }) + } +} diff --git a/services/web/test/acceptance/src/helpers/request.js b/services/web/test/acceptance/src/helpers/request.js new file mode 100644 index 0000000000..f1b1de4a0f --- /dev/null +++ b/services/web/test/acceptance/src/helpers/request.js @@ -0,0 +1,7 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +const BASE_URL = `http://${process.env['HTTP_TEST_HOST'] || 'localhost'}:3000` +module.exports = require('request').defaults({ + baseUrl: BASE_URL, + followRedirect: false +}) diff --git a/services/web/test/smoke/coffee/SmokeTests.coffee b/services/web/test/smoke/coffee/SmokeTests.coffee deleted file mode 100644 index d17c4ca0dd..0000000000 --- a/services/web/test/smoke/coffee/SmokeTests.coffee +++ /dev/null @@ -1,123 +0,0 @@ -child = require "child_process" -fs = require "fs" -assert = require("assert") -chai = require("chai") -chai.should() unless Object.prototype.should? -expect = chai.expect -Settings = require "settings-sharelatex" -ownPort = Settings.internal?.web?.port or Settings.port or 3000 -port = Settings.web?.web_router_port or ownPort # send requests to web router if this is the api process -cookeFilePath = "/tmp/smoke-test-cookie-#{ownPort}-to-#{port}.txt" -buildUrl = (path) -> " -b #{cookeFilePath} --resolve 'smoke#{Settings.cookieDomain}:#{port}:127.0.0.1' http://smoke#{Settings.cookieDomain}:#{port}/#{path}?setLng=en" -logger = require "logger-sharelatex" -LoginRateLimiter = require("../../../app/js/Features/Security/LoginRateLimiter.js") -RateLimiter = require("../../../app/js/infrastructure/RateLimiter.js") - -# Change cookie to be non secure so curl will send it -convertCookieFile = (callback) -> - fs = require("fs") - fs.readFile cookeFilePath, "utf8", (err, data) -> - return callback(err) if err - firstTrue = data.indexOf("TRUE") - secondTrue = data.indexOf("TRUE", firstTrue+4) - result = data.slice(0, secondTrue)+"FALSE"+data.slice(secondTrue+4) - fs.writeFile cookeFilePath, result, "utf8", (err) -> - return callback(err) if err - callback() - -describe "Opening", -> - - before (done) -> - logger.log "smoke test: setup" - LoginRateLimiter.recordSuccessfulLogin Settings.smokeTest.user, (err)-> - if err? - logger.err err:err, "smoke test: error recoring successful login" - return done(err) - RateLimiter.clearRateLimit "open-project", "#{Settings.smokeTest.projectId}:#{Settings.smokeTest.userId}", (err)-> - if err? - logger.err err:err, "smoke test: error clearing open-project rate limit" - return done(err) - RateLimiter.clearRateLimit "overleaf-login", Settings.smokeTest.rateLimitSubject, (err)-> - if err? - logger.err err:err, "smoke test: error clearing overleaf-login rate limit" - return done(err) - done() - return - - before (done) -> - logger.log "smoke test: hitting dev/csrf" - command = """ - curl -H "X-Forwarded-Proto: https" -c #{cookeFilePath} #{buildUrl('dev/csrf')} - """ - child.exec command, (err, stdout, stderr)-> - if err? then done(err) - csrf = stdout - logger.log "smoke test: converting cookie file 1" - convertCookieFile (err) -> - return done(err) if err? - logger.log "smoke test: hitting /login with csrf" - command = """ - curl -c #{cookeFilePath} -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"#{csrf}", "email":"#{Settings.smokeTest.user}", "password":"#{Settings.smokeTest.password}"}' #{buildUrl('login')} - """ - child.exec command, (err) -> - return done(err) if err? - logger.log "smoke test: finishing setup" - convertCookieFile done - return - - after (done)-> - logger.log "smoke test: converting cookie file 2" - convertCookieFile (err) -> - return done(err) if err? - logger.log "smoke test: cleaning up" - command = """ - curl -H "X-Forwarded-Proto: https" -c #{cookeFilePath} #{buildUrl('dev/csrf')} - """ - child.exec command, (err, stdout, stderr)-> - if err? then done(err) - csrf = stdout - logger.log "smoke test: converting cookie file 3" - convertCookieFile (err) -> - return done(err) if err? - command = """ - curl -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"#{csrf}"}' -c #{cookeFilePath} #{buildUrl('logout')} - """ - child.exec command, (err, stdout, stderr)-> - if err? - return done(err) - fs.unlink cookeFilePath, done - return - - it "a project", (done) -> - logger.log "smoke test: Checking can load a project" - @timeout(4000) - command = """ - curl -H "X-Forwarded-Proto: https" -v #{buildUrl("project/#{Settings.smokeTest.projectId}")} - """ - child.exec command, (error, stdout, stderr)-> - expect(error, "smoke test: error in getting project").to.not.exist - - statusCodeMatch = !!stderr.match("200 OK") - expect(statusCodeMatch, "smoke test: response code is not 200 getting project").to.equal true - - # Check that the project id is present in the javascript that loads up the project - match = !!stdout.match("window.project_id = \"#{Settings.smokeTest.projectId}\"") - expect(match, "smoke test: project page html does not have project_id").to.equal true - done() - - - it "the project list", (done) -> - logger.log "smoke test: Checking can load project list" - @timeout(4000) - command = """ - curl -H "X-Forwarded-Proto: https" -v #{buildUrl("project")} - """ - child.exec command, (error, stdout, stderr)-> - - expect(error, "smoke test: error returned in getting project list").to.not.exist - expect(!!stderr.match("200 OK"), "smoke test: response code is not 200 getting project list").to.equal true - expect(!!stdout.match("Your Projects - .*, Online LaTeX Editor"), "smoke test: body does not have correct title").to.equal true - expect(!!stdout.match("ProjectPageController"), "smoke test: body does not have correct angular controller").to.equal true - done() - - diff --git a/services/web/test/smoke/src/SmokeTests.js b/services/web/test/smoke/src/SmokeTests.js new file mode 100644 index 0000000000..509c5fea37 --- /dev/null +++ b/services/web/test/smoke/src/SmokeTests.js @@ -0,0 +1,235 @@ +/* eslint-disable + max-len, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const child = require('child_process') +let fs = require('fs') +const assert = require('assert') +const chai = require('chai') +if (Object.prototype.should == null) { + chai.should() +} +const { expect } = chai +const Settings = require('settings-sharelatex') +const ownPort = + __guard__( + Settings.internal != null ? Settings.internal.web : undefined, + x => x.port + ) || + Settings.port || + 3000 +const port = + (Settings.web != null ? Settings.web.web_router_port : undefined) || ownPort // send requests to web router if this is the api process +const cookeFilePath = `/tmp/smoke-test-cookie-${ownPort}-to-${port}.txt` +const buildUrl = path => + ` -b ${cookeFilePath} --resolve 'smoke${ + Settings.cookieDomain + }:${port}:127.0.0.1' http://smoke${ + Settings.cookieDomain + }:${port}/${path}?setLng=en` +const logger = require('logger-sharelatex') +const LoginRateLimiter = require('../../../app/src/Features/Security/LoginRateLimiter.js') +const RateLimiter = require('../../../app/src/infrastructure/RateLimiter.js') + +// Change cookie to be non secure so curl will send it +const convertCookieFile = function(callback) { + fs = require('fs') + return fs.readFile(cookeFilePath, 'utf8', function(err, data) { + if (err) { + return callback(err) + } + const firstTrue = data.indexOf('TRUE') + const secondTrue = data.indexOf('TRUE', firstTrue + 4) + const result = + data.slice(0, secondTrue) + 'FALSE' + data.slice(secondTrue + 4) + return fs.writeFile(cookeFilePath, result, 'utf8', function(err) { + if (err) { + return callback(err) + } + return callback() + }) + }) +} + +describe('Opening', function() { + before(function(done) { + logger.log('smoke test: setup') + LoginRateLimiter.recordSuccessfulLogin(Settings.smokeTest.user, function( + err + ) { + if (err != null) { + logger.err({ err }, 'smoke test: error recoring successful login') + return done(err) + } + return RateLimiter.clearRateLimit( + 'open-project', + `${Settings.smokeTest.projectId}:${Settings.smokeTest.userId}`, + function(err) { + if (err != null) { + logger.err( + { err }, + 'smoke test: error clearing open-project rate limit' + ) + return done(err) + } + return RateLimiter.clearRateLimit( + 'overleaf-login', + Settings.smokeTest.rateLimitSubject, + function(err) { + if (err != null) { + logger.err( + { err }, + 'smoke test: error clearing overleaf-login rate limit' + ) + return done(err) + } + return done() + } + ) + } + ) + }) + }) + + before(function(done) { + logger.log('smoke test: hitting dev/csrf') + let command = `\ +curl -H "X-Forwarded-Proto: https" -c ${cookeFilePath} ${buildUrl('dev/csrf')}\ +` + child.exec(command, function(err, stdout, stderr) { + if (err != null) { + done(err) + } + const csrf = stdout + logger.log('smoke test: converting cookie file 1') + return convertCookieFile(function(err) { + if (err != null) { + return done(err) + } + logger.log('smoke test: hitting /login with csrf') + command = `\ +curl -c ${cookeFilePath} -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"${csrf}", "email":"${ + Settings.smokeTest.user + }", "password":"${Settings.smokeTest.password}"}' ${buildUrl('login')}\ +` + return child.exec(command, function(err) { + if (err != null) { + return done(err) + } + logger.log('smoke test: finishing setup') + return convertCookieFile(done) + }) + }) + }) + }) + + after(function(done) { + logger.log('smoke test: converting cookie file 2') + convertCookieFile(function(err) { + if (err != null) { + return done(err) + } + logger.log('smoke test: cleaning up') + let command = `\ +curl -H "X-Forwarded-Proto: https" -c ${cookeFilePath} ${buildUrl('dev/csrf')}\ +` + return child.exec(command, function(err, stdout, stderr) { + if (err != null) { + done(err) + } + const csrf = stdout + logger.log('smoke test: converting cookie file 3') + return convertCookieFile(function(err) { + if (err != null) { + return done(err) + } + command = `\ +curl -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"${csrf}"}' -c ${cookeFilePath} ${buildUrl( + 'logout' + )}\ +` + return child.exec(command, function(err, stdout, stderr) { + if (err != null) { + return done(err) + } + return fs.unlink(cookeFilePath, done) + }) + }) + }) + }) + }) + + it('a project', function(done) { + logger.log('smoke test: Checking can load a project') + this.timeout(4000) + const command = `\ +curl -H "X-Forwarded-Proto: https" -v ${buildUrl( + `project/${Settings.smokeTest.projectId}` + )}\ +` + return child.exec(command, function(error, stdout, stderr) { + expect(error, 'smoke test: error in getting project').to.not.exist + + const statusCodeMatch = !!stderr.match('200 OK') + expect( + statusCodeMatch, + 'smoke test: response code is not 200 getting project' + ).to.equal(true) + + // Check that the project id is present in the javascript that loads up the project + const match = !!stdout.match( + `window.project_id = \"${Settings.smokeTest.projectId}\"` + ) + expect( + match, + 'smoke test: project page html does not have project_id' + ).to.equal(true) + return done() + }) + }) + + return it('the project list', function(done) { + logger.log('smoke test: Checking can load project list') + this.timeout(4000) + const command = `\ +curl -H "X-Forwarded-Proto: https" -v ${buildUrl('project')}\ +` + return child.exec(command, function(error, stdout, stderr) { + expect( + error, + 'smoke test: error returned in getting project list' + ).to.not.exist + expect( + !!stderr.match('200 OK'), + 'smoke test: response code is not 200 getting project list' + ).to.equal(true) + expect( + !!stdout.match( + 'Your Projects - .*, Online LaTeX Editor' + ), + 'smoke test: body does not have correct title' + ).to.equal(true) + expect( + !!stdout.match('ProjectPageController'), + 'smoke test: body does not have correct angular controller' + ).to.equal(true) + return done() + }) + }) +}) + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/test/unit/coffee/Analytics/AnalyticsControllerTests.coffee b/services/web/test/unit/coffee/Analytics/AnalyticsControllerTests.coffee deleted file mode 100644 index 0aa0517074..0000000000 --- a/services/web/test/unit/coffee/Analytics/AnalyticsControllerTests.coffee +++ /dev/null @@ -1,88 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -modulePath = path.join __dirname, '../../../../app/js/Features/Analytics/AnalyticsController' -sinon = require("sinon") -expect = require("chai").expect - - -describe 'AnalyticsController', -> - - beforeEach -> - @AuthenticationController = - getLoggedInUserId: sinon.stub() - - @AnalyticsManager = - updateEditingSession: sinon.stub().callsArgWith(3) - recordEvent: sinon.stub().callsArgWith(3) - - @InstitutionsAPI = - getInstitutionLicences: sinon.stub().callsArgWith(4) - - @controller = SandboxedModule.require modulePath, requires: - "./AnalyticsManager":@AnalyticsManager - "../Authentication/AuthenticationController":@AuthenticationController - "../Institutions/InstitutionsAPI":@InstitutionsAPI - "logger-sharelatex": - log:-> - '../../infrastructure/GeoIpLookup': @GeoIpLookup = - getDetails: sinon.stub() - - @res = - send:-> - - describe "updateEditingSession", -> - beforeEach -> - @req = - params: - projectId: "a project id" - @GeoIpLookup.getDetails = sinon.stub() - .callsArgWith(1, null, {country_code: 'XY'}) - - it "delegates to the AnalyticsManager", (done) -> - @AuthenticationController.getLoggedInUserId.returns("1234") - @controller.updateEditingSession @req, @res - - @AnalyticsManager.updateEditingSession.calledWith( - "1234", - "a project id", - 'XY' - ).should.equal true - done() - - describe "recordEvent", -> - beforeEach -> - @req = - params: - event:"i_did_something" - body:"stuff" - sessionID: "sessionIDHere" - session: {} - - it "should use the user_id", (done)-> - @AuthenticationController.getLoggedInUserId.returns("1234") - @controller.recordEvent @req, @res - @AnalyticsManager.recordEvent.calledWith("1234", @req.params["event"], @req.body).should.equal true - done() - - it "should use the session id", (done)-> - @controller.recordEvent @req, @res - @AnalyticsManager.recordEvent.calledWith(@req.sessionID, @req.params["event"], @req.body).should.equal true - done() - - describe "licences", -> - beforeEach -> - @req = - query: - resource_id:1 - start_date:'1514764800' - end_date:'1530662400' - resource_type:'institution' - sessionID: "sessionIDHere" - session: {} - - it "should trigger institutions api to fetch licences graph data", (done)-> - @controller.licences @req, @res - @InstitutionsAPI.getInstitutionLicences.calledWith(@req.query["resource_id"], @req.query["start_date"], @req.query["end_date"], @req.query["lag"]).should.equal true - done() diff --git a/services/web/test/unit/coffee/Announcement/AnnouncementsHandlerTests.coffee b/services/web/test/unit/coffee/Announcement/AnnouncementsHandlerTests.coffee deleted file mode 100644 index af2b579770..0000000000 --- a/services/web/test/unit/coffee/Announcement/AnnouncementsHandlerTests.coffee +++ /dev/null @@ -1,174 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -modulePath = path.join __dirname, '../../../../app/js/Features/Announcements/AnnouncementsHandler' -sinon = require("sinon") -expect = require("chai").expect - - -describe 'AnnouncementsHandler', -> - - beforeEach -> - @user = - _id:"3c6afe000000000000000000" #2002-02-14T00:00:00.000Z - email: "someone@gmail.com" - @AnalyticsManager = - getLastOccurrence: sinon.stub() - @BlogHandler = - getLatestAnnouncements:sinon.stub() - @settings = {} - @handler = SandboxedModule.require modulePath, requires: - "../Analytics/AnalyticsManager":@AnalyticsManager - "../Blog/BlogHandler":@BlogHandler - "settings-sharelatex":@settings - "logger-sharelatex": - log:-> - - describe "getUnreadAnnouncements", -> - beforeEach -> - @stubbedAnnouncements = [ - { - date: new Date(1478836800000), - id: '/2016/11/01/introducting-latex-code-checker' - }, { - date: new Date(1308369600000), - id: '/2013/08/02/thesis-series-pt1' - }, { - date: new Date(1108369600000), - id: '/2005/08/04/somethingelse' - }, { - date: new Date(1208369600000), - id: '/2008/04/12/title-date-irrelivant' - } - ] - @BlogHandler.getLatestAnnouncements.callsArgWith(0, null, @stubbedAnnouncements) - - - it "should mark all announcements as read is false", (done)-> - @AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) - @handler.getUnreadAnnouncements @user, (err, announcements)=> - announcements[0].read.should.equal false - announcements[1].read.should.equal false - announcements[2].read.should.equal false - announcements[3].read.should.equal false - done() - - it "should should be sorted again to ensure correct order", (done)-> - @AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) - @handler.getUnreadAnnouncements @user, (err, announcements)=> - announcements[3].should.equal @stubbedAnnouncements[2] - announcements[2].should.equal @stubbedAnnouncements[3] - announcements[1].should.equal @stubbedAnnouncements[1] - announcements[0].should.equal @stubbedAnnouncements[0] - done() - - it "should return older ones marked as read as well", (done)-> - @AnalyticsManager.getLastOccurrence.callsArgWith(2, null, {segmentation:{blogPostId:"/2008/04/12/title-date-irrelivant"}}) - @handler.getUnreadAnnouncements @user, (err, announcements)=> - announcements[0].id.should.equal @stubbedAnnouncements[0].id - announcements[0].read.should.equal false - - announcements[1].id.should.equal @stubbedAnnouncements[1].id - announcements[1].read.should.equal false - - announcements[2].id.should.equal @stubbedAnnouncements[3].id - announcements[2].read.should.equal true - - announcements[3].id.should.equal @stubbedAnnouncements[2].id - announcements[3].read.should.equal true - - done() - - it "should return all of them marked as read", (done)-> - @AnalyticsManager.getLastOccurrence.callsArgWith(2, null, {segmentation:{blogPostId:"/2016/11/01/introducting-latex-code-checker"}}) - @handler.getUnreadAnnouncements @user, (err, announcements)=> - announcements[0].read.should.equal true - announcements[1].read.should.equal true - announcements[2].read.should.equal true - announcements[3].read.should.equal true - done() - - it "should return posts older than signup date as read", (done)-> - @stubbedAnnouncements.push({ - date: new Date(978836800000), - id: '/2001/04/12/title-date-irrelivant' - }) - @AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) - @handler.getUnreadAnnouncements @user, (err, announcements)=> - announcements[0].read.should.equal false - announcements[1].read.should.equal false - announcements[2].read.should.equal false - announcements[3].read.should.equal false - announcements[4].read.should.equal true - announcements[4].id.should.equal '/2001/04/12/title-date-irrelivant' - done() - - - describe "with custom domain announcements", -> - beforeEach -> - @stubbedDomainSpecificAnn = [ - { - domains: ["gmail.com", 'yahoo.edu'] - title: "some message" - excerpt: "read this" - url:"http://www.sharelatex.com/i/somewhere" - id:"iaaa" - date: new Date(1308369600000).toString() - } - ] - - @handler._domainSpecificAnnouncements = sinon.stub().returns(@stubbedDomainSpecificAnn) - - it "should insert the domain specific in the correct place", (done)-> - @AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) - @handler.getUnreadAnnouncements @user, (err, announcements)=> - announcements[4].should.equal @stubbedAnnouncements[2] - announcements[3].should.equal @stubbedAnnouncements[3] - announcements[2].should.equal @stubbedAnnouncements[1] - announcements[1].should.equal @stubbedDomainSpecificAnn[0] - announcements[0].should.equal @stubbedAnnouncements[0] - done() - - describe "_domainSpecificAnnouncements", -> - beforeEach -> - @settings.domainAnnouncements = [ - { - domains: ["gmail.com", 'yahoo.edu'] - title: "some message" - excerpt: "read this" - url:"http://www.sharelatex.com/i/somewhere" - id:"id1" - date: new Date(1308369600000).toString() - }, { - domains: ["gmail.com", 'yahoo.edu'] - title: "some message" - excerpt: "read this" - url:"http://www.sharelatex.com/i/somewhere" - date: new Date(1308369600000).toString() - }, { - domains: ["gmail.com", 'yahoo.edu'] - title: "some message" - excerpt: "read this" - url:"http://www.sharelatex.com/i/somewhere" - id:"id3" - date: new Date(1308369600000).toString() - } - ] - - it "should filter announcments which don't have an id", (done) -> - result = @handler._domainSpecificAnnouncements "someone@gmail.com" - result.length.should.equal 2 - result[0].id.should.equal "id1" - result[1].id.should.equal "id3" - done() - - - it "should match on domain", (done) -> - @settings.domainAnnouncements[2].domains = ["yahoo.com"] - result = @handler._domainSpecificAnnouncements "someone@gmail.com" - result.length.should.equal 1 - result[0].id.should.equal "id1" - done() - - diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee deleted file mode 100644 index ba04b5d4b9..0000000000 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ /dev/null @@ -1,814 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Authentication/AuthenticationController.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -tk = require("timekeeper") -MockRequest = require("../helpers/MockRequest") -MockResponse = require("../helpers/MockResponse") -ObjectId = require("mongojs").ObjectId - -describe "AuthenticationController", -> - beforeEach -> - tk.freeze(Date.now()) - @UserModel = findOne: sinon.stub() - @AuthenticationController = SandboxedModule.require modulePath, requires: - "./AuthenticationManager": @AuthenticationManager = {} - "../User/UserUpdater" : @UserUpdater = {updateUser:sinon.stub()} - "metrics-sharelatex": @Metrics = { inc: sinon.stub() } - "../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() } - "../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()} - "../Analytics/AnalyticsManager": @AnalyticsManager = { recordEvent: sinon.stub() } - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err: sinon.stub() } - "settings-sharelatex": { siteUrl: 'http://www.foo.bar' } - "passport": @passport = - authenticate: sinon.stub().returns(sinon.stub()) - "../User/UserSessionsManager": @UserSessionsManager = - trackSession: sinon.stub() - untrackSession: sinon.stub() - revokeAllUserSessions: sinon.stub().callsArgWith(1, null) - "../../infrastructure/Modules": @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}} - "../SudoMode/SudoModeHandler": @SudoModeHandler = {activateSudoMode: sinon.stub().callsArgWith(1, null)} - "../Notifications/NotificationsBuilder": @NotificationsBuilder = - ipMatcherAffiliation: sinon.stub() - "../V1/V1Api": @V1Api = request: sinon.stub() - "../../models/User": { User: @UserModel } - "../../../../modules/oauth2-server/app/js/Oauth2Server": @Oauth2Server = - Request: sinon.stub() - Response: sinon.stub() - server: authenticate: sinon.stub() - @user = - _id: ObjectId() - email: @email = "USER@example.com" - first_name: "bob" - last_name: "brown" - referal_id: 1234 - isAdmin: false - @password = "banana" - @req = new MockRequest() - @res = new MockResponse() - @callback = @next = sinon.stub() - - afterEach -> - tk.reset() - - describe 'isUserLoggedIn', () -> - - beforeEach -> - @stub = sinon.stub(@AuthenticationController, 'getLoggedInUserId') - - afterEach -> - @stub.restore() - - it 'should do the right thing in all cases', () -> - @AuthenticationController.getLoggedInUserId.returns('some_id') - expect(@AuthenticationController.isUserLoggedIn(@req)).to.equal true - @AuthenticationController.getLoggedInUserId.returns(null) - expect(@AuthenticationController.isUserLoggedIn(@req)).to.equal false - @AuthenticationController.getLoggedInUserId.returns(false) - expect(@AuthenticationController.isUserLoggedIn(@req)).to.equal false - @AuthenticationController.getLoggedInUserId.returns(undefined) - expect(@AuthenticationController.isUserLoggedIn(@req)).to.equal false - - describe 'setInSessionUser', () -> - - beforeEach -> - @user = { - _id: 'id' - first_name: 'a' - last_name: 'b' - email: 'c' - } - @req.session.passport = {user: @user} - @req.session.user = @user - - it 'should update the right properties', () -> - @AuthenticationController.setInSessionUser(@req, {first_name: 'new_first_name', email: 'new_email'}) - expectedUser = { - _id: 'id' - first_name: 'new_first_name' - last_name: 'b' - email: 'new_email' - } - expect(@req.session.passport.user).to.deep.equal(expectedUser) - expect(@req.session.user).to.deep.equal(expectedUser) - - describe 'passportLogin', -> - - beforeEach -> - @info = null - @req.login = sinon.stub().callsArgWith(1, null) - @res.json = sinon.stub() - @req.session = @session = { - passport: {user: @user}, - postLoginRedirect: "/path/to/redir/to" - } - @req.session.destroy = sinon.stub().callsArgWith(0, null) - @req.session.save = sinon.stub().callsArgWith(0, null) - @req.sessionStore = {generate: sinon.stub()} - @AuthenticationController.finishLogin = sinon.stub() - @passport.authenticate.callsArgWith(1, null, @user, @info) - @err = new Error('woops') - - it 'should call passport.authenticate', () -> - @AuthenticationController.passportLogin @req, @res, @next - @passport.authenticate.callCount.should.equal 1 - - describe 'when authenticate produces an error', -> - - beforeEach -> - @passport.authenticate.callsArgWith(1, @err) - - it 'should return next with an error', () -> - @AuthenticationController.passportLogin @req, @res, @next - @next.calledWith(@err).should.equal true - - describe 'when authenticate produces a user', -> - - beforeEach -> - @req.session.postLoginRedirect = 'some_redirect' - @passport.authenticate.callsArgWith(1, null, @user, @info) - - afterEach -> - delete @req.session.postLoginRedirect - - it 'should call finishLogin', () -> - @AuthenticationController.passportLogin @req, @res, @next - @AuthenticationController.finishLogin.callCount.should.equal 1 - @AuthenticationController.finishLogin.calledWith(@user).should.equal true - - describe 'when authenticate does not produce a user', -> - - beforeEach -> - @info = {text: 'a', type: 'b'} - @passport.authenticate.callsArgWith(1, null, false, @info) - - it 'should not call finishLogin', () -> - @AuthenticationController.passportLogin @req, @res, @next - @AuthenticationController.finishLogin.callCount.should.equal 0 - - it 'should not send a json response with redirect', () -> - @AuthenticationController.passportLogin @req, @res, @next - @res.json.callCount.should.equal 1 - @res.json.calledWith({message: @info}).should.equal true - expect(@res.json.lastCall.args[0].redir?).to.equal false - - describe 'afterLoginSessionSetup', -> - - beforeEach -> - @req.login = sinon.stub().callsArgWith(1, null) - @req.session = @session = {passport: {user: @user}} - @req.session = - passport: {user: {_id: "one"}} - @req.session.destroy = sinon.stub().callsArgWith(0, null) - @req.session.save = sinon.stub().callsArgWith(0, null) - @req.sessionStore = {generate: sinon.stub()} - @UserSessionsManager.trackSession = sinon.stub() - @call = (callback) => - @AuthenticationController.afterLoginSessionSetup @req, @user, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.equal null - done() - - it 'should call req.login', (done) -> - @call (err) => - @req.login.callCount.should.equal 1 - done() - - it 'should call req.session.save', (done) -> - @call (err) => - @req.session.save.callCount.should.equal 1 - done() - - it 'should call UserSessionsManager.trackSession', (done) -> - @call (err) => - @UserSessionsManager.trackSession.callCount.should.equal 1 - done() - - describe 'when req.session.save produces an error', -> - - beforeEach -> - @req.session.save = sinon.stub().callsArgWith(0, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.not.be.oneOf [null, undefined] - expect(err).to.be.instanceof Error - done() - - it 'should not call UserSessionsManager.trackSession', (done) -> - @call (err) => - @UserSessionsManager.trackSession.callCount.should.equal 0 - done() - - describe 'getSessionUser', -> - - it 'should get the user object from session', -> - @req.session = - passport: - user: {_id: 'one'} - user = @AuthenticationController.getSessionUser(@req) - expect(user).to.deep.equal {_id: 'one'} - - it 'should work with legacy sessions', -> - @req.session = - user: {_id: 'one'} - user = @AuthenticationController.getSessionUser(@req) - expect(user).to.deep.equal {_id: 'one'} - - describe "doPassportLogin", -> - beforeEach -> - @AuthenticationController._recordFailedLogin = sinon.stub() - @AuthenticationController._recordSuccessfulLogin = sinon.stub() - @Modules.hooks.fire = sinon.stub().callsArgWith(3, null, []) - # @AuthenticationController.establishUserSession = sinon.stub().callsArg(2) - @req.body = - email: @email - password: @password - session: - postLoginRedirect: "/path/to/redir/to" - @cb = sinon.stub() - - describe "when the preDoPassportLogin hooks produce an info object", -> - beforeEach -> - @Modules.hooks.fire = sinon.stub().callsArgWith(3, null, [null, {redir: '/somewhere'}, null]) - - it "should stop early and call done with this info object", (done) -> - @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) - @cb.callCount.should.equal 1 - @cb.calledWith(null, false, {redir: '/somewhere'}).should.equal true - @LoginRateLimiter.processLoginRequest.callCount.should.equal 0 - done() - - describe "when the users rate limit", -> - - beforeEach -> - @LoginRateLimiter.processLoginRequest.callsArgWith(1, null, false) - - it "should block the request if the limit has been exceeded", (done)-> - @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) - @cb.callCount.should.equal 1 - @cb.calledWith(null, null).should.equal true - done() - - describe 'when the user is authenticated', -> - beforeEach -> - @cb = sinon.stub() - @LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true) - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) - @req.sessionID = Math.random() - @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) - - it "should attempt to authorise the user", -> - @AuthenticationManager.authenticate - .calledWith(email: @email.toLowerCase(), @password) - .should.equal true - - it "should establish the user's session", -> - @cb.calledWith(null, @user).should.equal true - - describe '_loginAsyncHandlers', -> - beforeEach -> - @UserHandler.setupLoginData = sinon.stub() - @LoginRateLimiter.recordSuccessfulLogin = sinon.stub() - @AuthenticationController._recordSuccessfulLogin = sinon.stub() - @AnalyticsManager.recordEvent = sinon.stub() - @AnalyticsManager.identifyUser = sinon.stub() - @AuthenticationController._loginAsyncHandlers(@req, @user) - - it "should call identifyUser", -> - @AnalyticsManager.identifyUser.calledWith(@user._id, @req.sessionID).should.equal true - - it "should setup the user data in the background", -> - @UserHandler.setupLoginData.calledWith(@user).should.equal true - - it "should set res.session.justLoggedIn", -> - @req.session.justLoggedIn.should.equal true - - it "should record the successful login", -> - @AuthenticationController._recordSuccessfulLogin - .calledWith(@user._id) - .should.equal true - - it "should tell the rate limiter that there was a success for that email", -> - @LoginRateLimiter.recordSuccessfulLogin.calledWith(@user.email).should.equal true - - it "should log the successful login", -> - @logger.log - .calledWith(email: @user.email, user_id: @user._id.toString(), "successful log in") - .should.equal true - - it "should track the login event", -> - @AnalyticsManager.recordEvent - .calledWith(@user._id, "user-logged-in") - .should.equal true - - describe 'when the user is not authenticated', -> - beforeEach -> - @LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true) - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null) - @cb = sinon.stub() - @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb) - - it "should not establish the login", -> - @cb.callCount.should.equal 1 - @cb.calledWith(null, false) - # @res.body.should.exist - expect(@cb.lastCall.args[2]).to.contain.all.keys ['text', 'type'] - # message: - # text: 'Your email or password were incorrect. Please try again', - # type: 'error' - - it "should not setup the user data in the background", -> - @UserHandler.setupLoginData.called.should.equal false - - it "should record a failed login", -> - @AuthenticationController._recordFailedLogin.called.should.equal true - - it "should log the failed login", -> - @logger.log - .calledWith(email: @email.toLowerCase(), "failed log in") - .should.equal true - - describe "getLoggedInUserId", -> - - beforeEach -> - @req = - session :{} - - it "should return the user id from the session", ()-> - @user_id = "2134" - @req.session.user = - _id:@user_id - result = @AuthenticationController.getLoggedInUserId @req - expect(result).to.equal @user_id - - it 'should return user for passport session', () -> - @user_id = "2134" - @req.session = { - passport: { - user: { - _id:@user_id - } - } - } - result = @AuthenticationController.getLoggedInUserId @req - expect(result).to.equal @user_id - - it "should return null if there is no user on the session", ()-> - result = @AuthenticationController.getLoggedInUserId @req - expect(result).to.equal null - - it "should return null if there is no session", ()-> - @req = {} - result = @AuthenticationController.getLoggedInUserId @req - expect(result).to.equal null - - it "should return null if there is no req", ()-> - @req = {} - result = @AuthenticationController.getLoggedInUserId @req - expect(result).to.equal null - - describe "requireLogin", -> - beforeEach -> - @user = - _id: "user-id-123" - email: "user@sharelatex.com" - @middleware = @AuthenticationController.requireLogin() - - describe "when the user is logged in", -> - beforeEach -> - @req.session = - user: @user = { - _id: "user-id-123" - email: "user@sharelatex.com" - } - @middleware(@req, @res, @next) - - it "should call the next method in the chain", -> - @next.called.should.equal true - - describe "when the user is not logged in", -> - beforeEach -> - @req.session = {} - @AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() - @req.query = {} - @middleware(@req, @res, @next) - - it "should redirect to the register or login page", -> - @AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true - - describe "requireOauth", -> - beforeEach -> - @res.sendStatus = sinon.stub() - @res.send = sinon.stub() - @res.status = sinon.stub().returns(@res) - @res.sendStatus = sinon.stub() - @middleware = @AuthenticationController.requireOauth() - - describe "when Oauth2Server authenticates", -> - beforeEach -> - @token = - accessToken: "token" - user: "user" - @Oauth2Server.server.authenticate.yields null, @token - @middleware(@req, @res, @next) - - it "should set oauth_token on request", -> - @req.oauth_token.should.equal @token - - it "should set oauth on request", -> - @req.oauth.access_token.should.equal @token.accessToken - - it "should set oauth_user on request", -> - @req.oauth_user.should.equal "user" - - it "should call next", -> - @next.should.have.been.calledOnce - - describe "when Oauth2Server does not authenticate", -> - beforeEach -> - @Oauth2Server.server.authenticate.yields code: 401 - - describe "when token not provided", -> - beforeEach -> - @middleware(@req, @res, @next) - - it "should return 401 error", -> - @res.sendStatus.should.have.been.calledWith 401 - - describe "when token provided", -> - beforeEach -> - @V1Api.request = sinon.stub().yields("error", {}, {}) - @req.token = "foo" - @middleware(@req, @res, @next) - - it "should make request to v1 api with token", -> - @V1Api.request.should.have.been.calledWith { - expectedStatusCodes: [401] - json: token: "foo" - method: "POST" - uri: "/api/v1/sharelatex/oauth_authorize" - } - - describe "when v1 api returns error", -> - beforeEach -> - @V1Api.request = sinon.stub().yields("error", {}, {}) - @req.token = "foo" - @middleware(@req, @res, @next) - - it "should return status", -> - @next.should.have.been.calledWith "error" - - describe "when v1 api status code is not 200", -> - beforeEach -> - @V1Api.request = sinon.stub().yields(null, {statusCode: 401}, {}) - @req.token = "foo" - @middleware(@req, @res, @next) - - it "should return status", -> - @res.status.should.have.been.calledWith 401 - - describe "when v1 api returns authorized profile and access token", -> - beforeEach -> - @oauth_authorize = - access_token: "access_token" - user_profile: id: "overleaf-id" - @V1Api.request = sinon.stub().yields(null, {statusCode: 200}, @oauth_authorize) - @req.token = "foo" - - describe "in all cases", -> - beforeEach -> - @middleware(@req, @res, @next) - - it "should find user", -> - @UserModel.findOne.should.have.been.calledWithMatch { "overleaf.id": "overleaf-id" } - - describe "when user find returns error", -> - beforeEach -> - @UserModel.findOne = sinon.stub().yields("error") - @middleware(@req, @res, @next) - - it "should return error", -> - @next.should.have.been.calledWith "error" - - describe "when user is not found", -> - beforeEach -> - @UserModel.findOne = sinon.stub().yields(null, null) - @middleware(@req, @res, @next) - - it "should return unauthorized", -> - @res.status.should.have.been.calledWith 401 - - describe "when user is found", -> - beforeEach -> - @UserModel.findOne = sinon.stub().yields(null, "user") - @middleware(@req, @res, @next) - - it "should add user to request", -> - @req.oauth_user.should.equal "user" - - it "should add access_token to request", -> - @req.oauth.access_token.should.equal "access_token" - - describe "requireGlobalLogin", -> - beforeEach -> - @req.headers = {} - @AuthenticationController.httpAuth = sinon.stub() - @setRedirect = sinon.spy(@AuthenticationController, 'setRedirectInSession') - - afterEach -> - @setRedirect.restore() - - describe "with white listed url", -> - beforeEach -> - @AuthenticationController.addEndpointToLoginWhitelist "/login" - @req._parsedUrl.pathname = "/login" - @AuthenticationController.requireGlobalLogin @req, @res, @next - - it "should call next() directly", -> - @next.called.should.equal true - - describe "with white listed url and a query string", -> - beforeEach -> - @AuthenticationController.addEndpointToLoginWhitelist "/login" - @req._parsedUrl.pathname = "/login" - @req.url = "/login?query=something" - @AuthenticationController.requireGlobalLogin @req, @res, @next - - it "should call next() directly", -> - @next.called.should.equal true - - describe "with http auth", -> - beforeEach -> - @req.headers["authorization"] = "Mock Basic Auth" - @AuthenticationController.requireGlobalLogin @req, @res, @next - - it "should pass the request onto httpAuth", -> - @AuthenticationController.httpAuth - .calledWith(@req, @res, @next) - .should.equal true - - describe "with a user session", -> - beforeEach -> - @req.session = - user: {"mock": "user", "_id": "some_id"} - @AuthenticationController.requireGlobalLogin @req, @res, @next - - it "should call next() directly", -> - @next.called.should.equal true - - describe "with no login credentials", -> - beforeEach -> - @req.session = {} - @AuthenticationController.requireGlobalLogin @req, @res, @next - - it 'should have called setRedirectInSession', -> - @setRedirect.callCount.should.equal 1 - - it "should redirect to the /login page", -> - @res.redirectedTo.should.equal "/login" - - describe "_redirectToLoginOrRegisterPage", -> - beforeEach -> - @middleware = @AuthenticationController.requireLogin(@options = { load_from_db: false }) - @req.session = {} - @AuthenticationController._redirectToRegisterPage = sinon.stub() - @AuthenticationController._redirectToLoginPage = sinon.stub() - @req.query = {} - - describe "they have come directly to the url", -> - beforeEach -> - @req.query = {} - @middleware(@req, @res, @next) - - it "should redirect to the login page", -> - @AuthenticationController._redirectToRegisterPage.calledWith(@req, @res).should.equal false - @AuthenticationController._redirectToLoginPage.calledWith(@req, @res).should.equal true - - describe "they have come via a templates link", -> - - beforeEach -> - @req.query.zipUrl = "something" - @middleware(@req, @res, @next) - - it "should redirect to the register page", -> - @AuthenticationController._redirectToRegisterPage.calledWith(@req, @res).should.equal true - @AuthenticationController._redirectToLoginPage.calledWith(@req, @res).should.equal false - - describe "they have been invited to a project", -> - - beforeEach -> - @req.query.project_name = "something" - @middleware(@req, @res, @next) - - it "should redirect to the register page", -> - @AuthenticationController._redirectToRegisterPage.calledWith(@req, @res).should.equal true - @AuthenticationController._redirectToLoginPage.calledWith(@req, @res).should.equal false - - describe "_redirectToRegisterPage", -> - beforeEach -> - @req.path = "/target/url" - @req.query = - extra_query: "foo" - @AuthenticationController._redirectToRegisterPage(@req, @res) - - it "should redirect to the register page with a query string attached", -> - @req.session.postLoginRedirect.should.equal '/target/url?extra_query=foo' - @res.redirectedTo.should.equal "/register?extra_query=foo" - - it "should log out a message", -> - @logger.log - .calledWith(url: @url, "user not logged in so redirecting to register page") - .should.equal true - - describe "_redirectToLoginPage", -> - beforeEach -> - @req.path = "/target/url" - @req.query = - extra_query: "foo" - @AuthenticationController._redirectToLoginPage(@req, @res) - - it "should redirect to the register page with a query string attached", -> - @req.session.postLoginRedirect.should.equal '/target/url?extra_query=foo' - @res.redirectedTo.should.equal "/login?extra_query=foo" - - - describe "_recordSuccessfulLogin", -> - beforeEach -> - @UserUpdater.updateUser = sinon.stub().callsArg(2) - @AuthenticationController._recordSuccessfulLogin(@user._id, @callback) - - it "should increment the user.login.success metric", -> - @Metrics.inc - .calledWith("user.login.success") - .should.equal true - - it "should update the user's login count and last logged in date", -> - @UserUpdater.updateUser.args[0][1]["$set"]["lastLoggedIn"].should.not.equal undefined - @UserUpdater.updateUser.args[0][1]["$inc"]["loginCount"].should.equal 1 - - it "should call the callback", -> - @callback.called.should.equal true - - describe "_recordFailedLogin", -> - beforeEach -> - @AuthenticationController._recordFailedLogin(@callback) - - it "should increment the user.login.failed metric", -> - @Metrics.inc - .calledWith("user.login.failed") - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - - describe 'setRedirectInSession', -> - beforeEach -> - @req = {session: {}} - @req.path = "/somewhere" - @req.query = {one: "1"} - - it 'should set redirect property on session', -> - @AuthenticationController.setRedirectInSession(@req) - expect(@req.session.postLoginRedirect).to.equal "/somewhere?one=1" - - it 'should set the supplied value', -> - @AuthenticationController.setRedirectInSession(@req, '/somewhere/specific') - expect(@req.session.postLoginRedirect).to.equal "/somewhere/specific" - - it 'should not allow open redirects', -> - @AuthenticationController.setRedirectInSession(@req, 'https://evil.com') - expect(@req.session.postLoginRedirect).to.be.undefined - - describe 'with a png', -> - beforeEach -> - @req = {session: {}} - - it 'should not set the redirect', -> - @AuthenticationController.setRedirectInSession(@req, '/something.png') - expect(@req.session.postLoginRedirect).to.equal undefined - - describe 'with a js path', -> - - beforeEach -> - @req = {session: {}} - - it 'should not set the redirect', -> - @AuthenticationController.setRedirectInSession(@req, '/js/something.js') - expect(@req.session.postLoginRedirect).to.equal undefined - - describe '_getRedirectFromSession', -> - it 'should get redirect property from session', -> - @req = session: { postLoginRedirect: '/a?b=c' } - expect(@AuthenticationController._getRedirectFromSession(@req)).to.equal "/a?b=c" - - it 'should not allow open redirects', -> - @req = session: { postLoginRedirect: 'https://evil.com' } - expect(@AuthenticationController._getRedirectFromSession(@req)).to.be.null - - it 'handle null values', -> - @req = session: {} - expect(@AuthenticationController._getRedirectFromSession(@req)).to.be.null - - describe '_getSafeRedirectPath', -> - it 'sanitize redirect path to prevent open redirects', -> - expect( - @AuthenticationController._getSafeRedirectPath('https://evil.com') - ).to.be.undefined - - expect( - @AuthenticationController._getSafeRedirectPath('//evil.com') - ).to.be.undefined - - expect( - @AuthenticationController._getSafeRedirectPath('//ol.com/evil') - ).to.equal '/evil' - - expect( - @AuthenticationController._getSafeRedirectPath('////evil.com') - ).to.be.undefined - - expect( - @AuthenticationController._getSafeRedirectPath('%2F%2Fevil.com') - ).to.equal '/%2F%2Fevil.com' - - expect( - @AuthenticationController._getSafeRedirectPath('.evil.com') - ).to.equal '/.evil.com' - - describe '_clearRedirectFromSession', -> - beforeEach -> - @req = {session: {postLoginRedirect: "/a?b=c"}} - - it 'should remove the redirect property from session', -> - @AuthenticationController._clearRedirectFromSession(@req) - expect(@req.session.postLoginRedirect).to.equal undefined - - - describe 'finishLogin', -> - # - get redirect - # - async handlers - # - afterLoginSessionSetup - # - clear redirect - # - issue redir, two ways - beforeEach -> - @AuthenticationController._getRedirectFromSession = sinon.stub().returns '/some/page' - @AuthenticationController._loginAsyncHandlers = sinon.stub() - @AuthenticationController.afterLoginSessionSetup = sinon.stub().callsArgWith(2, null) - @AuthenticationController._clearRedirectFromSession = sinon.stub() - @AuthenticationController._redirectToReconfirmPage = sinon.stub() - @req.headers = {accept: 'application/json, whatever'} - @res.json = sinon.stub() - @res.redirect = sinon.stub() - - it 'should extract the redirect from the session', () -> - @AuthenticationController.finishLogin(@user, @req, @res, @next) - expect(@AuthenticationController._getRedirectFromSession.callCount).to.equal 1 - expect(@AuthenticationController._getRedirectFromSession.calledWith(@req)).to.equal true - - it 'should call the async handlers', () -> - @AuthenticationController.finishLogin(@user, @req, @res, @next) - expect(@AuthenticationController._loginAsyncHandlers.callCount).to.equal 1 - expect(@AuthenticationController._loginAsyncHandlers.calledWith(@req, @user)).to.equal true - - it 'should call afterLoginSessionSetup', () -> - @AuthenticationController.finishLogin(@user, @req, @res, @next) - expect(@AuthenticationController.afterLoginSessionSetup.callCount).to.equal 1 - expect(@AuthenticationController.afterLoginSessionSetup.calledWith(@req, @user)).to.equal true - - it 'should clear redirect from session', () -> - @AuthenticationController.finishLogin(@user, @req, @res, @next) - expect(@AuthenticationController._clearRedirectFromSession.callCount).to.equal 1 - expect(@AuthenticationController._clearRedirectFromSession.calledWith(@req)).to.equal true - - it 'should issue a json response with a redirect', () -> - @AuthenticationController.finishLogin(@user, @req, @res, @next) - expect(@res.json.callCount).to.equal 1 - expect(@res.redirect.callCount).to.equal 0 - expect(@res.json.calledWith({ redir: '/some/page' })).to.equal true - - describe 'with a non-json request', -> - beforeEach -> - @req.headers = {} - @res.json = sinon.stub() - @res.redirect = sinon.stub() - - it 'should issue a plain redirect', () -> - @AuthenticationController.finishLogin(@user, @req, @res, @next) - expect(@res.json.callCount).to.equal 0 - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith('/some/page')).to.equal true - - describe "when user is flagged to reconfirm", -> - beforeEach -> - @req.session = {} - @user.must_reconfirm = true - it "should redirect to reconfirm page", () -> - @AuthenticationController.finishLogin(@user, @req, @res, @next) - expect(@AuthenticationController._redirectToReconfirmPage.calledWith(@req)).to.equal true \ No newline at end of file diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationManagerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationManagerTests.coffee deleted file mode 100644 index 1dea07f29a..0000000000 --- a/services/web/test/unit/coffee/Authentication/AuthenticationManagerTests.coffee +++ /dev/null @@ -1,382 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Authentication/AuthenticationManager.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -ObjectId = require("mongojs").ObjectId -Errors = require "../../../../app/js/Features/Errors/Errors" - -describe "AuthenticationManager", -> - beforeEach -> - @settings = { security: { bcryptRounds: 12 } } - @AuthenticationManager = SandboxedModule.require modulePath, requires: - "../../models/User": User: @User = {} - "../../infrastructure/mongojs": - db: @db = - users: {} - ObjectId: ObjectId - "bcrypt": @bcrypt = {} - "settings-sharelatex": @settings - "../V1/V1Handler": @V1Handler = {} - "../User/UserGetter": @UserGetter = {} - @callback = sinon.stub() - - describe "with real bcrypt", -> - beforeEach -> - bcrypt = require('bcrypt') - @bcrypt.compare = bcrypt.compare - @bcrypt.getRounds = bcrypt.getRounds - @bcrypt.genSalt = bcrypt.genSalt - @bcrypt.hash = bcrypt.hash - # Hash of 'testpassword' - @testPassword = '$2a$12$zhtThy3R5tLtw5sCwr5XD.zhPENGn4ecjeMcP87oYSYrIICFqBpei' - - describe "authenticate", -> - beforeEach -> - @user = - _id: "user-id" - email: @email = "USER@sharelatex.com" - @User.findOne = sinon.stub().callsArgWith(1, null, @user) - - describe "when the hashed password matches", -> - beforeEach (done) -> - @unencryptedPassword = "testpassword" - @user.hashedPassword = @testPassword - @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) => - @callback(error, user) - done() - - it "should look up the correct user in the database", -> - @User.findOne.calledWith(email: @email).should.equal true - - it "should return the user", -> - @callback.calledWith(null, @user).should.equal true - - describe "when the encrypted passwords do not match", -> - beforeEach -> - @AuthenticationManager.authenticate(email: @email, "notthecorrectpassword", @callback) - - it "should not return the user", -> - @callback.calledWith(null, null).should.equal true - - describe "setUserPasswordInV2", -> - beforeEach -> - @user = - _id: "5c8791477192a80b5e76ca7e" - email: @email = "USER@sharelatex.com" - @db.users.update = sinon.stub().callsArgWith(2, null, {nModified: 1}) - - it "should not produce an error", (done) -> - @AuthenticationManager.setUserPasswordInV2 @user._id, "testpassword", (err, updated) => - expect(err).to.not.exist - expect(updated).to.equal true - done() - - it "should set the hashed password", (done) -> - @AuthenticationManager.setUserPasswordInV2 @user._id, "testpassword", (err, updated) => - expect(err).to.not.exist - hashedPassword = @db.users.update.lastCall.args[1].$set.hashedPassword - expect(hashedPassword).to.exist - expect(hashedPassword.length).to.equal 60 - expect(hashedPassword).to.match /^\$2a\$12\$[a-zA-Z0-9\/.]{53}$/ - done() - - describe "authenticate", -> - describe "when the user exists in the database", -> - beforeEach -> - @user = - _id: "user-id" - email: @email = "USER@sharelatex.com" - @unencryptedPassword = "banana" - @User.findOne = sinon.stub().callsArgWith(1, null, @user) - - describe "when the hashed password matches", -> - beforeEach (done) -> - @user.hashedPassword = @hashedPassword = "asdfjadflasdf" - @bcrypt.compare = sinon.stub().callsArgWith(2, null, true) - @bcrypt.getRounds = sinon.stub().returns 12 - @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) => - @callback(error, user) - done() - - it "should look up the correct user in the database", -> - @User.findOne.calledWith(email: @email).should.equal true - - it "should check that the passwords match", -> - @bcrypt.compare - .calledWith(@unencryptedPassword, @hashedPassword) - .should.equal true - - it "should return the user", -> - @callback.calledWith(null, @user).should.equal true - - describe "when the encrypted passwords do not match", -> - beforeEach -> - @AuthenticationManager.authenticate(email: @email, @unencryptedPassword, @callback) - - it "should not return the user", -> - @callback.calledWith(null, null).should.equal true - - describe "when the hashed password matches but the number of rounds is too low", -> - beforeEach (done) -> - @user.hashedPassword = @hashedPassword = "asdfjadflasdf" - @bcrypt.compare = sinon.stub().callsArgWith(2, null, true) - @bcrypt.getRounds = sinon.stub().returns 7 - @AuthenticationManager.setUserPassword = sinon.stub().callsArgWith(2, null) - @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) => - @callback(error, user) - done() - - it "should look up the correct user in the database", -> - @User.findOne.calledWith(email: @email).should.equal true - - it "should check that the passwords match", -> - @bcrypt.compare - .calledWith(@unencryptedPassword, @hashedPassword) - .should.equal true - - it "should check the number of rounds", -> - @bcrypt.getRounds.called.should.equal true - - it "should set the users password (with a higher number of rounds)", -> - @AuthenticationManager.setUserPassword - .calledWith("user-id", @unencryptedPassword) - .should.equal true - - it "should return the user", -> - @callback.calledWith(null, @user).should.equal true - - describe "when the hashed password matches but the number of rounds is too low, but upgrades disabled", -> - beforeEach (done) -> - @settings.security.disableBcryptRoundsUpgrades = true - @user.hashedPassword = @hashedPassword = "asdfjadflasdf" - @bcrypt.compare = sinon.stub().callsArgWith(2, null, true) - @bcrypt.getRounds = sinon.stub().returns 7 - @AuthenticationManager.setUserPassword = sinon.stub().callsArgWith(2, null) - @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) => - @callback(error, user) - done() - - it "should not check the number of rounds", -> - @bcrypt.getRounds.called.should.equal false - - it "should not set the users password (with a higher number of rounds)", -> - @AuthenticationManager.setUserPassword - .calledWith("user-id", @unencryptedPassword) - .should.equal false - - it "should return the user", -> - @callback.calledWith(null, @user).should.equal true - - describe "when the user does not exist in the database", -> - beforeEach -> - @User.findOne = sinon.stub().callsArgWith(1, null, null) - @AuthenticationManager.authenticate(email: @email, @unencrpytedPassword, @callback) - - it "should not return a user", -> - @callback.calledWith(null, null).should.equal true - - describe "validateEmail", -> - describe "valid", -> - it "should return null", -> - result = @AuthenticationManager.validateEmail 'foo@example.com' - expect(result).to.equal null - - describe "invalid", -> - it "should return validation error object for no email", -> - result = @AuthenticationManager.validateEmail '' - expect(result).to.not.equal null - expect(result.message).to.equal 'email not valid' - - it "should return validation error object for invalid", -> - result = @AuthenticationManager.validateEmail 'notanemail' - expect(result).to.not.equal null - expect(result.message).to.equal 'email not valid' - - describe "validatePassword", -> - beforeEach -> - # 73 characters: - @longPassword = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345678' - - describe "with a null password", -> - it "should return an error", -> - expect(@AuthenticationManager.validatePassword()).to.eql { message: 'password not set' } - - describe "password length", -> - describe "with the default password length options", -> - it "should reject passwords that are too short", -> - expect(@AuthenticationManager.validatePassword('')).to.eql { message: 'password is too short' } - expect(@AuthenticationManager.validatePassword('foo')).to.eql { message: 'password is too short' } - - it "should reject passwords that are too long", -> - expect(@AuthenticationManager.validatePassword(@longPassword)).to.eql { message: 'password is too long' } - - it "should accept passwords that are a good length", -> - expect(@AuthenticationManager.validatePassword('l337h4x0r')).to.equal null - - describe "when the password length is specified in settings", -> - beforeEach -> - @settings.passwordStrengthOptions = - length: - min: 10 - max: 12 - - it "should reject passwords that are too short", -> - expect(@AuthenticationManager.validatePassword('012345678')).to.eql { message: 'password is too short' } - - it "should accept passwords of exactly minimum length", -> - expect(@AuthenticationManager.validatePassword('0123456789')).to.equal null - - it "should reject passwords that are too long", -> - expect(@AuthenticationManager.validatePassword('0123456789abc')).to.eql { message: 'password is too long' } - - it "should accept passwords of exactly maximum length", -> - expect(@AuthenticationManager.validatePassword('0123456789ab')).to.equal null - - describe "when the maximum password length is set to >72 characters in settings", -> - beforeEach -> - @settings.passwordStrengthOptions = - length: - max: 128 - - it "should still reject passwords > 72 characters in length", -> - expect(@AuthenticationManager.validatePassword(@longPassword)).to.eql { message: 'password is too long' } - - describe "allowed characters", -> - describe "with the default settings for allowed characters", -> - it "should allow passwords with valid characters", -> - expect(@AuthenticationManager.validatePassword("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")).to.equal null - expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.equal null - - it "should not allow passwords with invalid characters", -> - expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.eql { message: 'password contains an invalid character' } - - describe "when valid characters are overridden in settings", -> - beforeEach -> - @settings.passwordStrengthOptions = - chars: - symbols: " " - - it "should allow passwords with valid characters", -> - expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.equal null - - it "should disallow passwords with invalid characters", -> - expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.eql { message: 'password contains an invalid character' } - - describe "when allowAnyChars is set", -> - beforeEach -> - @settings.passwordStrengthOptions = - allowAnyChars: true - - it "should allow any characters", -> - expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.equal null - expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.equal null - - describe "setUserPassword", -> - beforeEach -> - @user_id = ObjectId() - @password = "banana" - @hashedPassword = "asdkjfa;osiuvandf" - @salt = "saltaasdfasdfasdf" - @bcrypt.genSalt = sinon.stub().callsArgWith(2, null, @salt) - @bcrypt.hash = sinon.stub().callsArgWith(2, null, @hashedPassword) - @db.users.update = sinon.stub().callsArg(2) - - describe "too long", -> - beforeEach -> - @settings.passwordStrengthOptions = - length: - max:10 - @password = "dsdsadsadsadsadsadkjsadjsadjsadljs" - - it "should return and error", (done)-> - @AuthenticationManager.setUserPassword @user_id, @password, (err)-> - expect(err).to.exist - done() - - it "should not start the bcrypt process", (done)-> - @AuthenticationManager.setUserPassword @user_id, @password, (err)=> - @bcrypt.genSalt.called.should.equal false - @bcrypt.hash.called.should.equal false - done() - - describe "too short", -> - beforeEach -> - @settings.passwordStrengthOptions = - length: - max:10 - min:6 - @password = "dsd" - - it "should return and error", (done)-> - @AuthenticationManager.setUserPassword @user_id, @password, (err)-> - expect(err).to.exist - done() - - it "should not start the bcrypt process", (done)-> - @AuthenticationManager.setUserPassword @user_id, @password, (err)=> - @bcrypt.genSalt.called.should.equal false - @bcrypt.hash.called.should.equal false - done() - - describe "password set attempt", -> - describe "with SL user in SL", -> - beforeEach -> - @UserGetter.getUser = sinon.stub().yields(null, { overleaf: null }) - @AuthenticationManager.setUserPassword(@user_id, @password, @callback) - - it 'should look up the user', -> - @UserGetter.getUser.calledWith(@user_id).should.equal true - - it "should update the user's password in the database", -> - args = @db.users.update.lastCall.args - expect(args[0]).to.deep.equal {_id: ObjectId(@user_id.toString())} - expect(args[1]).to.deep.equal { - $set: { - "hashedPassword": @hashedPassword - } - $unset: password: true - } - - it "should hash the password", -> - @bcrypt.genSalt - .calledWith(12) - .should.equal true - @bcrypt.hash - .calledWith(@password, @salt) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "with SL user in v2", -> - beforeEach (done) -> - @settings.overleaf = true - @UserGetter.getUser = sinon.stub().yields(null, { overleaf: null }) - @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) => - @callback(err, changed) - done() - it "should error", -> - @callback.calledWith(new Errors.SLInV2Error("Password Reset Attempt")).should.equal true - - describe "with v2 user in SL", -> - beforeEach (done) -> - @UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} }) - @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) => - @callback(err, changed) - done() - it "should error", -> - @callback.calledWith(new Errors.NotInV2Error("Password Reset Attempt")).should.equal true - - describe "with v2 user in v2", -> - beforeEach (done) -> - @settings.overleaf = true - @UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} }) - @V1Handler.doPasswordReset = sinon.stub().yields(null, true) - @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) => - @callback(err, changed) - done() - it "should set the password in v2", -> - @callback.calledWith(null, true).should.equal true diff --git a/services/web/test/unit/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/unit/coffee/Authorization/AuthorizationManagerTests.coffee deleted file mode 100644 index 063d5bdd26..0000000000 --- a/services/web/test/unit/coffee/Authorization/AuthorizationManagerTests.coffee +++ /dev/null @@ -1,525 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Authorization/AuthorizationManager.js" -SandboxedModule = require('sandboxed-module') -Errors = require "../../../../app/js/Features/Errors/Errors.js" - -describe "AuthorizationManager", -> - beforeEach -> - @AuthorizationManager = SandboxedModule.require modulePath, requires: - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} - '../Project/ProjectGetter': @ProjectGetter = {} - "../../models/User": User: @User = {} - "../Errors/Errors": Errors - "../TokenAccess/TokenAccessHandler": @TokenAccessHandler = { - isValidToken: sinon.stub().callsArgWith(2, null, false, false) - } - @user_id = "user-id-1" - @project_id = "project-id-1" - @token = 'some-token' - @callback = sinon.stub() - - describe "getPrivilegeLevelForProject", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub() - @AuthorizationManager.isUserSiteAdmin = sinon.stub() - @CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub() - - describe 'with a token-based project', -> - beforeEach -> - @ProjectGetter.getProject - .withArgs(@project_id, { publicAccesLevel: 1 }) - .yields(null, { publicAccesLevel: "tokenBased" }) - - describe "with a user_id with a privilege level", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, "readOnly") - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return the user's privilege level", -> - @callback.calledWith(null, "readOnly", false, false).should.equal true - - describe "with a user_id with no privilege level", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, false) - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return false", -> - @callback.calledWith(null, false, false, false).should.equal true - - describe "with a user_id who is an admin", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, true) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, false) - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return the user as an owner", -> - @callback.calledWith(null, "owner", false, true).should.equal true - - describe "with no user (anonymous)", -> - - describe 'when the token is not valid', -> - - beforeEach -> - @TokenAccessHandler.isValidToken = sinon.stub() - .withArgs(@project_id, @token) - .yields(null, false, false) - @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @token, @callback - - it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> - @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false - - it "should not call AuthorizationManager.isUserSiteAdmin", -> - @AuthorizationManager.isUserSiteAdmin.called.should.equal false - - it 'should check if the token is valid', -> - @TokenAccessHandler.isValidToken.calledWith(@project_id, @token).should.equal true - - it "should return false", -> - @callback.calledWith(null, false, false, false).should.equal true - - describe 'when the token is valid for read-and-write', -> - - describe 'when read-write-sharing is not enabled', -> - beforeEach -> - @TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false - @TokenAccessHandler.isValidToken = sinon.stub() - .withArgs(@project_id, @token) - .yields(null, true, false) - @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @token, @callback - - it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> - @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false - - it "should not call AuthorizationManager.isUserSiteAdmin", -> - @AuthorizationManager.isUserSiteAdmin.called.should.equal false - - it 'should check if the token is valid', -> - @TokenAccessHandler.isValidToken.calledWith(@project_id, @token).should.equal true - - it "should deny access", -> - @callback.calledWith(null, false, false, false).should.equal true - - describe 'when read-write-sharing is enabled', -> - beforeEach -> - @TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true - @TokenAccessHandler.isValidToken = sinon.stub() - .withArgs(@project_id, @token) - .yields(null, true, false) - @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @token, @callback - - it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> - @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false - - it "should not call AuthorizationManager.isUserSiteAdmin", -> - @AuthorizationManager.isUserSiteAdmin.called.should.equal false - - it 'should check if the token is valid', -> - @TokenAccessHandler.isValidToken.calledWith(@project_id, @token).should.equal true - - it "should give read-write access", -> - @callback.calledWith(null, "readAndWrite", false).should.equal true - - describe 'when the token is valid for read-only', -> - - beforeEach -> - @TokenAccessHandler.isValidToken = sinon.stub() - .withArgs(@project_id, @token) - .yields(null, false, true) - @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @token, @callback - - it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> - @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false - - it "should not call AuthorizationManager.isUserSiteAdmin", -> - @AuthorizationManager.isUserSiteAdmin.called.should.equal false - - it 'should check if the token is valid', -> - @TokenAccessHandler.isValidToken.calledWith(@project_id, @token).should.equal true - - it "should give read-only access", -> - @callback.calledWith(null, "readOnly", false).should.equal true - - describe "with a private project", -> - beforeEach -> - @ProjectGetter.getProject - .withArgs(@project_id, { publicAccesLevel: 1 }) - .yields(null, { publicAccesLevel: "private" }) - - describe "with a user_id with a privilege level", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, "readOnly") - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return the user's privilege level", -> - @callback.calledWith(null, "readOnly", false, false).should.equal true - - describe "with a user_id with no privilege level", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, false) - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return false", -> - @callback.calledWith(null, false, false, false).should.equal true - - describe "with a user_id who is an admin", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, true) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, false) - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return the user as an owner", -> - @callback.calledWith(null, "owner", false, true).should.equal true - - describe "with no user (anonymous)", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @token, @callback - - it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> - @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false - - it "should not call AuthorizationManager.isUserSiteAdmin", -> - @AuthorizationManager.isUserSiteAdmin.called.should.equal false - - it "should return false", -> - @callback.calledWith(null, false, false, false).should.equal true - - describe "with a public project", -> - beforeEach -> - @ProjectGetter.getProject - .withArgs(@project_id, { publicAccesLevel: 1 }) - .yields(null, { publicAccesLevel: "readAndWrite" }) - - describe "with a user_id with a privilege level", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, "readOnly") - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return the user's privilege level", -> - @callback.calledWith(null, "readOnly", false).should.equal true - - describe "with a user_id with no privilege level", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, false) - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return the public privilege level", -> - @callback.calledWith(null, "readAndWrite", true).should.equal true - - describe "with a user_id who is an admin", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, true) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, false) - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, @callback - - it "should return the user as an owner", -> - @callback.calledWith(null, "owner", false).should.equal true - - describe "with no user (anonymous)", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @token, @callback - - it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> - @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false - - it "should not call AuthorizationManager.isUserSiteAdmin", -> - @AuthorizationManager.isUserSiteAdmin.called.should.equal false - - it "should return the public privilege level", -> - @callback.calledWith(null, "readAndWrite", true).should.equal true - - describe "when the project doesn't exist", -> - beforeEach -> - @ProjectGetter.getProject - .withArgs(@project_id, { publicAccesLevel: 1 }) - .yields(null, null) - - it "should return a NotFoundError", -> - @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @token, (error) -> - error.should.be.instanceof Errors.NotFoundError - - describe "when the project id is not valid", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) - @CollaboratorsHandler.getMemberIdPrivilegeLevel - .withArgs(@user_id, @project_id) - .yields(null, "readOnly") - - it "should return a error", (done)-> - @AuthorizationManager.getPrivilegeLevelForProject undefined, "not project id", @token, (err) => - @ProjectGetter.getProject.called.should.equal false - expect(err).to.exist - done() - - describe "canUserReadProject", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() - - describe "when user is owner", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "owner", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserReadProject @user_id, @project_id, @token, (error, canRead) -> - expect(canRead).to.equal true - done() - - describe "when user has read-write access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readAndWrite", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserReadProject @user_id, @project_id, @token, (error, canRead) -> - expect(canRead).to.equal true - done() - - describe "when user has read-only access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readOnly", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserReadProject @user_id, @project_id, @token, (error, canRead) -> - expect(canRead).to.equal true - done() - - describe "when user has no access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, false, false) - - it "should return false", (done) -> - @AuthorizationManager.canUserReadProject @user_id, @project_id, @token, (error, canRead) -> - expect(canRead).to.equal false - done() - - describe "canUserWriteProjectContent", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() - - describe "when user is owner", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "owner", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal true - done() - - describe "when user has read-write access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readAndWrite", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal true - done() - - describe "when user has read-only access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readOnly", false) - - it "should return false", (done) -> - @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal false - done() - - describe "when user has no access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, false, false) - - it "should return false", (done) -> - @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal false - done() - - describe "canUserWriteProjectSettings", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() - - describe "when user is owner", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "owner", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal true - done() - - describe "when user has read-write access as a collaborator", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readAndWrite", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal true - done() - - describe "when user has read-write access as the public", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readAndWrite", true) - - it "should return false", (done) -> - @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal false - done() - - describe "when user has read-only access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readOnly", false) - - it "should return false", (done) -> - @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal false - done() - - describe "when user has no access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, false, false) - - it "should return false", (done) -> - @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, @token, (error, canWrite) -> - expect(canWrite).to.equal false - done() - - describe "canUserAdminProject", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() - - describe "when user is owner", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "owner", false) - - it "should return true", (done) -> - @AuthorizationManager.canUserAdminProject @user_id, @project_id, @token, (error, canAdmin) -> - expect(canAdmin).to.equal true - done() - - describe "when user has read-write access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readAndWrite", false) - - it "should return false", (done) -> - @AuthorizationManager.canUserAdminProject @user_id, @project_id, @token, (error, canAdmin) -> - expect(canAdmin).to.equal false - done() - - describe "when user has read-only access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, "readOnly", false) - - it "should return false", (done) -> - @AuthorizationManager.canUserAdminProject @user_id, @project_id, @token, (error, canAdmin) -> - expect(canAdmin).to.equal false - done() - - describe "when user has no access", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject - .withArgs(@user_id, @project_id, @token) - .yields(null, false, false) - - it "should return false", (done) -> - @AuthorizationManager.canUserAdminProject @user_id, @project_id, @token, (error, canAdmin) -> - expect(canAdmin).to.equal false - done() - - describe "isUserSiteAdmin", -> - beforeEach -> - @User.findOne = sinon.stub() - - describe "when user is admin", -> - beforeEach -> - @User.findOne - .withArgs({ _id: @user_id }, { isAdmin: 1 }) - .yields(null, { isAdmin: true }) - - it "should return true", (done) -> - @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> - expect(isAdmin).to.equal true - done() - - describe "when user is not admin", -> - beforeEach -> - @User.findOne - .withArgs({ _id: @user_id }, { isAdmin: 1 }) - .yields(null, { isAdmin: false }) - - it "should return false", (done) -> - @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> - expect(isAdmin).to.equal false - done() - - describe "when user is not found", -> - beforeEach -> - @User.findOne - .withArgs({ _id: @user_id }, { isAdmin: 1 }) - .yields(null, null) - - it "should return false", (done) -> - @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> - expect(isAdmin).to.equal false - done() - - describe "when no user is passed", -> - it "should return false", (done) -> - @AuthorizationManager.isUserSiteAdmin null, (error, isAdmin) => - @User.findOne.called.should.equal false - expect(isAdmin).to.equal false - done() diff --git a/services/web/test/unit/coffee/Authorization/AuthorizationMiddlewareTests.coffee b/services/web/test/unit/coffee/Authorization/AuthorizationMiddlewareTests.coffee deleted file mode 100644 index 39f1eb6a3b..0000000000 --- a/services/web/test/unit/coffee/Authorization/AuthorizationMiddlewareTests.coffee +++ /dev/null @@ -1,274 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Authorization/AuthorizationMiddleware.js" -SandboxedModule = require('sandboxed-module') -Errors = require "../../../../app/js/Features/Errors/Errors.js" - -describe "AuthorizationMiddleware", -> - beforeEach -> - @user_id = "user-id-123" - @project_id = "project-id-123" - @token = 'some-token' - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user_id) - isUserLoggedIn: sinon.stub().returns(true) - @AuthorizationMiddleware = SandboxedModule.require modulePath, requires: - "./AuthorizationManager": @AuthorizationManager = {} - "logger-sharelatex": {log: () ->} - "mongojs": ObjectId: @ObjectId = {} - "../Errors/Errors": Errors - '../Authentication/AuthenticationController': @AuthenticationController - "../TokenAccess/TokenAccessHandler": @TokenAccessHandler = - getRequestToken: sinon.stub().returns(@token) - @req = {} - @res = {} - @ObjectId.isValid = sinon.stub() - @ObjectId.isValid.withArgs(@project_id).returns true - @next = sinon.stub() - - describe "_getUserId", -> - beforeEach -> - @req = {} - - it "should get the user from session", (done) -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns("1234") - @AuthorizationMiddleware._getUserId @req, (err, user_id) => - expect(err).to.not.exist - expect(user_id).to.equal "1234" - done() - - it "should get oauth_user from request", (done) -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(null) - @req.oauth_user = {_id: "5678"} - @AuthorizationMiddleware._getUserId @req, (err, user_id) => - expect(err).to.not.exist - expect(user_id).to.equal "5678" - done() - - it "should fall back to null", (done) -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(null) - @req.oauth_user = undefined - @AuthorizationMiddleware._getUserId @req, (err, user_id) => - expect(err).to.not.exist - expect(user_id).to.equal null - done() - - METHODS_TO_TEST = { - "ensureUserCanReadProject": "canUserReadProject" - "ensureUserCanWriteProjectSettings": "canUserWriteProjectSettings" - "ensureUserCanWriteProjectContent": "canUserWriteProjectContent" - "ensureUserCanAdminProject": "canUserAdminProject" - } - for middlewareMethod, managerMethod of METHODS_TO_TEST - do (middlewareMethod, managerMethod) -> - describe middlewareMethod, -> - beforeEach -> - @req.params = - project_id: @project_id - @AuthorizationManager[managerMethod] = sinon.stub() - @AuthorizationMiddleware.redirectToRestricted = sinon.stub() - - describe "with missing project_id", -> - beforeEach -> - @req.params = {} - - it "should return an error to next", -> - @AuthorizationMiddleware[middlewareMethod] @req, @res, @next - @next.calledWith(new Error()).should.equal true - - describe "with logged in user", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(@user_id) - - describe "when user has permission", -> - beforeEach -> - @AuthorizationManager[managerMethod] - .withArgs(@user_id, @project_id, @token) - .yields(null, true) - - it "should return next", -> - @AuthorizationMiddleware[middlewareMethod] @req, @res, @next - @next.called.should.equal true - - describe "when user doesn't have permission", -> - beforeEach -> - @AuthorizationManager[managerMethod] - .withArgs(@user_id, @project_id, @token) - .yields(null, false) - - it "should redirect to redirectToRestricted", -> - @AuthorizationMiddleware[middlewareMethod] @req, @res, @next - @next.called.should.equal false - @AuthorizationMiddleware.redirectToRestricted - .calledWith(@req, @res, @next) - .should.equal true - - describe "with anonymous user", -> - describe "when user has permission", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(null) - @AuthorizationManager[managerMethod] - .withArgs(null, @project_id, @token) - .yields(null, true) - - it "should return next", -> - @AuthorizationMiddleware[middlewareMethod] @req, @res, @next - @next.called.should.equal true - - describe "when user doesn't have permission", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(null) - @AuthorizationManager[managerMethod] - .withArgs(null, @project_id, @token) - .yields(null, false) - - it "should redirect to redirectToRestricted", -> - @AuthorizationMiddleware[middlewareMethod] @req, @res, @next - @next.called.should.equal false - @AuthorizationMiddleware.redirectToRestricted - .calledWith(@req, @res, @next) - .should.equal true - - describe "with malformed project id", -> - beforeEach -> - @req.params = - project_id: "blah" - @ObjectId.isValid = sinon.stub().returns false - - it "should return a not found error", (done) -> - @AuthorizationMiddleware[middlewareMethod] @req, @res, (error) -> - error.should.be.instanceof Errors.NotFoundError - done() - - describe "ensureUserIsSiteAdmin", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin = sinon.stub() - @AuthorizationMiddleware.redirectToRestricted = sinon.stub() - - describe "with logged in user", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(@user_id) - - describe "when user has permission", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin - .withArgs(@user_id) - .yields(null, true) - - it "should return next", -> - @AuthorizationMiddleware.ensureUserIsSiteAdmin @req, @res, @next - @next.called.should.equal true - - describe "when user doesn't have permission", -> - beforeEach -> - @AuthorizationManager.isUserSiteAdmin - .withArgs(@user_id) - .yields(null, false) - - it "should redirect to redirectToRestricted", -> - @AuthorizationMiddleware.ensureUserIsSiteAdmin @req, @res, @next - @next.called.should.equal false - @AuthorizationMiddleware.redirectToRestricted - .calledWith(@req, @res, @next) - .should.equal true - - describe "with anonymous user", -> - describe "when user has permission", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(null) - @AuthorizationManager.isUserSiteAdmin - .withArgs(null) - .yields(null, true) - - it "should return next", -> - @AuthorizationMiddleware.ensureUserIsSiteAdmin @req, @res, @next - @next.called.should.equal true - - describe "when user doesn't have permission", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(null) - @AuthorizationManager.isUserSiteAdmin - .withArgs(null) - .yields(null, false) - - it "should redirect to redirectToRestricted", -> - @AuthorizationMiddleware.ensureUserIsSiteAdmin @req, @res, @next - @next.called.should.equal false - @AuthorizationMiddleware.redirectToRestricted - .calledWith(@req, @res, @next) - .should.equal true - - describe "ensureUserCanReadMultipleProjects", -> - beforeEach -> - @AuthorizationManager.canUserReadProject = sinon.stub() - @AuthorizationMiddleware.redirectToRestricted = sinon.stub() - @req.query = - project_ids: "project1,project2" - - describe "with logged in user", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(@user_id) - - describe "when user has permission to access all projects", -> - beforeEach -> - @AuthorizationManager.canUserReadProject - .withArgs(@user_id, "project1", @token) - .yields(null, true) - @AuthorizationManager.canUserReadProject - .withArgs(@user_id, "project2", @token) - .yields(null, true) - - it "should return next", -> - @AuthorizationMiddleware.ensureUserCanReadMultipleProjects @req, @res, @next - @next.called.should.equal true - - describe "when user doesn't have permission to access one of the projects", -> - beforeEach -> - @AuthorizationManager.canUserReadProject - .withArgs(@user_id, "project1", @token) - .yields(null, true) - @AuthorizationManager.canUserReadProject - .withArgs(@user_id, "project2", @token) - .yields(null, false) - - it "should redirect to redirectToRestricted", -> - @AuthorizationMiddleware.ensureUserCanReadMultipleProjects @req, @res, @next - @next.called.should.equal false - @AuthorizationMiddleware.redirectToRestricted - .calledWith(@req, @res, @next) - .should.equal true - - describe "with anonymous user", -> - describe "when user has permission", -> - describe "when user has permission to access all projects", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(null) - @AuthorizationManager.canUserReadProject - .withArgs(null, "project1", @token) - .yields(null, true) - @AuthorizationManager.canUserReadProject - .withArgs(null, "project2", @token) - .yields(null, true) - - it "should return next", -> - @AuthorizationMiddleware.ensureUserCanReadMultipleProjects @req, @res, @next - @next.called.should.equal true - - describe "when user doesn't have permission to access one of the projects", -> - beforeEach -> - @AuthenticationController.getLoggedInUserId.returns(null) - @AuthorizationManager.canUserReadProject - .withArgs(null, "project1", @token) - .yields(null, true) - @AuthorizationManager.canUserReadProject - .withArgs(null, "project2", @token) - .yields(null, false) - - it "should redirect to redirectToRestricted", -> - @AuthorizationMiddleware.ensureUserCanReadMultipleProjects @req, @res, @next - @next.called.should.equal false - @AuthorizationMiddleware.redirectToRestricted - .calledWith(@req, @res, @next) - .should.equal true diff --git a/services/web/test/unit/coffee/BetaProgram/BetaProgramControllerTests.coffee b/services/web/test/unit/coffee/BetaProgram/BetaProgramControllerTests.coffee deleted file mode 100644 index 713179b056..0000000000 --- a/services/web/test/unit/coffee/BetaProgram/BetaProgramControllerTests.coffee +++ /dev/null @@ -1,143 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/BetaProgram/BetaProgramController" -expect = require("chai").expect - -describe "BetaProgramController", -> - - beforeEach -> - @user = - _id: @user_id = "a_simple_id" - email: "user@example.com" - features: {} - betaProgram: false - @req = - query: {} - session: - user: @user - @BetaProgramController = SandboxedModule.require modulePath, requires: - "./BetaProgramHandler": @BetaProgramHandler = { - optIn: sinon.stub() - optOut: sinon.stub() - }, - "../User/UserGetter": @UserGetter = { - getUser: sinon.stub() - }, - "settings-sharelatex": @settings = { - languages: {} - } - "logger-sharelatex": @logger = { - log: sinon.stub() - err: sinon.stub() - error: sinon.stub() - } - '../Authentication/AuthenticationController': @AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(@user._id) - } - @res = - send: sinon.stub() - redirect: sinon.stub() - render: sinon.stub() - @next = sinon.stub() - - describe "optIn", -> - - beforeEach -> - @BetaProgramHandler.optIn.callsArgWith(1, null) - - it "should redirect to '/beta/participate'", () -> - @BetaProgramController.optIn @req, @res, @next - @res.redirect.callCount.should.equal 1 - @res.redirect.firstCall.args[0].should.equal "/beta/participate" - - it "should not call next with an error", () -> - @BetaProgramController.optIn @req, @res, @next - @next.callCount.should.equal 0 - - it "should not call next with an error", () -> - @BetaProgramController.optIn @req, @res, @next - @next.callCount.should.equal 0 - - it "should call BetaProgramHandler.optIn", () -> - @BetaProgramController.optIn @req, @res, @next - @BetaProgramHandler.optIn.callCount.should.equal 1 - - describe "when BetaProgramHandler.opIn produces an error", -> - - beforeEach -> - @BetaProgramHandler.optIn.callsArgWith(1, new Error('woops')) - - it "should not redirect to '/beta/participate'", () -> - @BetaProgramController.optIn @req, @res, @next - @res.redirect.callCount.should.equal 0 - - it "should produce an error", () -> - @BetaProgramController.optIn @req, @res, @next - @next.callCount.should.equal 1 - @next.firstCall.args[0].should.be.instanceof Error - - describe "optOut", -> - - beforeEach -> - @BetaProgramHandler.optOut.callsArgWith(1, null) - - it "should redirect to '/beta/participate'", () -> - @BetaProgramController.optOut @req, @res, @next - @res.redirect.callCount.should.equal 1 - @res.redirect.firstCall.args[0].should.equal "/beta/participate" - - it "should not call next with an error", () -> - @BetaProgramController.optOut @req, @res, @next - @next.callCount.should.equal 0 - - it "should not call next with an error", () -> - @BetaProgramController.optOut @req, @res, @next - @next.callCount.should.equal 0 - - it "should call BetaProgramHandler.optOut", () -> - @BetaProgramController.optOut @req, @res, @next - @BetaProgramHandler.optOut.callCount.should.equal 1 - - describe "when BetaProgramHandler.optOut produces an error", -> - - beforeEach -> - @BetaProgramHandler.optOut.callsArgWith(1, new Error('woops')) - - it "should not redirect to '/beta/participate'", () -> - @BetaProgramController.optOut @req, @res, @next - @res.redirect.callCount.should.equal 0 - - it "should produce an error", () -> - @BetaProgramController.optOut @req, @res, @next - @next.callCount.should.equal 1 - @next.firstCall.args[0].should.be.instanceof Error - - - describe "optInPage", -> - - beforeEach -> - @UserGetter.getUser.callsArgWith(1, null, @user) - - it "should render the opt-in page", () -> - @BetaProgramController.optInPage @req, @res, @next - @res.render.callCount.should.equal 1 - args = @res.render.firstCall.args - args[0].should.equal 'beta_program/opt_in' - - - describe "when UserGetter.getUser produces an error", -> - - beforeEach -> - @UserGetter.getUser.callsArgWith(1, new Error('woops')) - - it "should not render the opt-in page", () -> - @BetaProgramController.optInPage @req, @res, @next - @res.render.callCount.should.equal 0 - - it "should produce an error", () -> - @BetaProgramController.optInPage @req, @res, @next - @next.callCount.should.equal 1 - @next.firstCall.args[0].should.be.instanceof Error diff --git a/services/web/test/unit/coffee/BetaProgram/BetaProgramHandlerTests.coffee b/services/web/test/unit/coffee/BetaProgram/BetaProgramHandlerTests.coffee deleted file mode 100644 index 2a1a1b1629..0000000000 --- a/services/web/test/unit/coffee/BetaProgram/BetaProgramHandlerTests.coffee +++ /dev/null @@ -1,100 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -modulePath = path.join __dirname, '../../../../app/js/Features/BetaProgram/BetaProgramHandler' -sinon = require("sinon") -expect = require("chai").expect - - -describe 'BetaProgramHandler', -> - - beforeEach -> - @user_id = "some_id" - @user = - _id: @user_id - email: 'user@example.com' - features: {} - betaProgram: false - save: sinon.stub().callsArgWith(0, null) - @handler = SandboxedModule.require modulePath, requires: - "../../models/User": { - User: - findById: sinon.stub().callsArgWith(1, null, @user) - }, - "logger-sharelatex": @logger = { - log: sinon.stub() - err: sinon.stub() - }, - "metrics-sharelatex": @logger = { - inc: sinon.stub() - } - - - describe "optIn", -> - - beforeEach -> - @user.betaProgram = false - @call = (callback) => - @handler.optIn @user_id, callback - - it "should set betaProgram = true on user object", (done) -> - @call (err) => - @user.betaProgram.should.equal true - done() - - it "should call user.save", (done) -> - @call (err) => - @user.save.callCount.should.equal 1 - done() - - it "should not produce an error", (done) -> - @call (err) => - expect(err).to.equal null - expect(err).to.not.be.instanceof Error - done() - - describe "when user.save produces an error", -> - - beforeEach -> - @user.save.callsArgWith(0, new Error('woops')) - - it "should produce an error", (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - describe "optOut", -> - - beforeEach -> - @user.betaProgram = true - @call = (callback) => - @handler.optOut @user_id, callback - - it "should set betaProgram = true on user object", (done) -> - @call (err) => - @user.betaProgram.should.equal false - done() - - it "should call user.save", (done) -> - @call (err) => - @user.save.callCount.should.equal 1 - done() - - it "should not produce an error", (done) -> - @call (err) => - expect(err).to.equal null - expect(err).to.not.be.instanceof Error - done() - - describe "when user.save produces an error", -> - - beforeEach -> - @user.save.callsArgWith(0, new Error('woops')) - - it "should produce an error", (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() diff --git a/services/web/test/unit/coffee/Blog/BlogControllerTests.coffee b/services/web/test/unit/coffee/Blog/BlogControllerTests.coffee deleted file mode 100644 index 5c17f6d939..0000000000 --- a/services/web/test/unit/coffee/Blog/BlogControllerTests.coffee +++ /dev/null @@ -1,72 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Blog/BlogController" -expect = require("chai").expect - -describe "BlogController", -> - - beforeEach -> - - @settings = - apis: - blog: - url:"http://blog.sharelatex.env" - @request = - get: sinon.stub() - @ErrorController = {} - @BlogController = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - "../Errors/ErrorController": @ErrorController - "request": @request - - @req = {} - @res = {} - - - describe "getPage", ()-> - - it "should get the data from the blog api", (done)-> - @req.url = "/blog/something.html" - body = {"stuff":"here"} - - @request.get.callsArgWith(1, null, null, JSON.stringify(body)) - @res.render = (view, data)=> - @request.get.calledWith("#{@settings.apis.blog.url}#{@req.url}") - view.should.equal "blog/blog_holder" - assert.deepEqual body, data - done() - - @BlogController.getPage @req, @res - - it "should send to the error controller if the blog responds 404", (done)-> - @req.url = "/blog/something.html" - @request.get.callsArgWith(1, null, {statusCode:404}) - - @ErrorController.notFound = (req, res)=> - assert.deepEqual req, @req - assert.deepEqual res, @res - done() - - @BlogController.getPage @req, @res - - it "should proxy the image urls", (done)-> - @BlogController._directProxy = sinon.stub() - @req.url = "/something.png" - @BlogController.getPage @req, @res - @BlogController._directProxy.calledWith("#{@settings.apis.blog.url}#{@req.url}", @res).should.equal true - done() - - - describe "getIndexPage", -> - - it "should change the url and send it to getPage", (done)-> - @req.url = "/blog" - @BlogController.getPage = (req, res)-> - req.url.should.equal "/blog/index.html" - done() - @BlogController.getIndexPage @req, @res - diff --git a/services/web/test/unit/coffee/BrandVariations/BrandVariationsHandlerTests.coffee b/services/web/test/unit/coffee/BrandVariations/BrandVariationsHandlerTests.coffee deleted file mode 100644 index 2c4ad1772c..0000000000 --- a/services/web/test/unit/coffee/BrandVariations/BrandVariationsHandlerTests.coffee +++ /dev/null @@ -1,59 +0,0 @@ -expect = require("chai").expect -SandboxedModule = require("sandboxed-module") -assert = require("assert") -path = require("path") -sinon = require("sinon") -expect = require("chai").expect -modulePath = path.join __dirname, "../../../../app/js/Features/BrandVariations/BrandVariationsHandler" - -describe "BrandVariationsHandler", -> - - beforeEach -> - @settings = - apis: - v1: - url: "http://overleaf.example.com" - @logger = - err: -> - log: -> - @V1Api = - request: sinon.stub() - @BrandVariationsHandler = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "logger-sharelatex": @logger - "../V1/V1Api": @V1Api - @mockedBrandVariationDetails = - id: "12" - active: true - brand_name: "The journal" - logo_url: "http://my.cdn.tld/journal-logo.png" - journal_cover_url: "http://my.cdn.tld/journal-cover.jpg" - home_url: "http://www.thejournal.com/" - publish_menu_link_html: "Submit your paper to the The Journal" - - describe "getBrandVariationById", -> - it "should call the callback with an error when the branding variation id is not provided", (done) -> - @BrandVariationsHandler.getBrandVariationById null, (err, brandVariationDetails) => - expect(err).to.be.instanceof Error - done() - - it "should call the callback with an error when the request errors", (done) -> - @V1Api.request.callsArgWith 1, new Error() - @BrandVariationsHandler.getBrandVariationById "12", (err, brandVariationDetails) => - expect(err).to.be.instanceof Error - done() - - it "should call the callback with branding details when request succeeds", (done) -> - @V1Api.request.callsArgWith 1, null, { statusCode: 200 }, @mockedBrandVariationDetails - @BrandVariationsHandler.getBrandVariationById "12", (err, brandVariationDetails) => - expect(err).to.not.exist - expect(brandVariationDetails).to.deep.equal @mockedBrandVariationDetails - done() - - it "should transform relative URLs in v1 absolute ones", (done) -> - @mockedBrandVariationDetails.logo_url = "/journal-logo.png" - @V1Api.request.callsArgWith 1, null, { statusCode: 200 }, @mockedBrandVariationDetails - @BrandVariationsHandler.getBrandVariationById "12", (err, brandVariationDetails) => - expect(brandVariationDetails.logo_url.startsWith(@settings.apis.v1.url)).to.be.true - done() - diff --git a/services/web/test/unit/coffee/Chat/ChatApiHandlerTests.coffee b/services/web/test/unit/coffee/Chat/ChatApiHandlerTests.coffee deleted file mode 100644 index ea569b8a53..0000000000 --- a/services/web/test/unit/coffee/Chat/ChatApiHandlerTests.coffee +++ /dev/null @@ -1,92 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatApiHandler" -expect = require("chai").expect - -describe "ChatApiHandler", -> - beforeEach -> - @settings = - apis: - chat: - internal_url:"chat.sharelatex.env" - @request = sinon.stub() - @ChatApiHandler = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "logger-sharelatex": { log: sinon.stub(), error: sinon.stub() } - "request": @request - @project_id = "3213213kl12j" - @user_id = "2k3jlkjs9" - @content = "my message here" - @callback = sinon.stub() - - describe "sendGlobalMessage", -> - describe "successfully", -> - beforeEach -> - @message = { "mock": "message" } - @request.callsArgWith(1, null, {statusCode: 200}, @message) - @ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback - - it "should post the data to the chat api", -> - @request.calledWith({ - url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages" - method: "POST" - json: - content: @content - user_id: @user_id - }).should.equal true - - it "should return the message from the post", -> - @callback.calledWith(null, @message).should.equal true - - describe "with a non-success status code", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 500}) - @ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback - - it "should return an error", -> - error = new Error() - error.statusCode = 500 - @callback.calledWith(error).should.equal true - - describe "getGlobalMessages", -> - beforeEach -> - @messages = [{ "mock": "message" }] - @limit = 30 - @before = "1234" - - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 200}, @messages) - @ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback - - it "should make get request for room to chat api", -> - @request.calledWith({ - method: "GET" - url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages" - qs: - limit: @limit - before: @before - json: true - }).should.equal true - - it "should return the messages from the request", -> - @callback.calledWith(null, @messages).should.equal true - - describe "with failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 500}, null) - @ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback - - it "should return an error", -> - error = new Error() - error.statusCode = 500 - @callback.calledWith(error).should.equal true - - - - - - diff --git a/services/web/test/unit/coffee/Chat/ChatControllerTests.coffee b/services/web/test/unit/coffee/Chat/ChatControllerTests.coffee deleted file mode 100644 index be83de686e..0000000000 --- a/services/web/test/unit/coffee/Chat/ChatControllerTests.coffee +++ /dev/null @@ -1,156 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatController" -expect = require("chai").expect - -describe "ChatController", -> - beforeEach -> - @user_id = 'mock-user-id' - @settings = {} - @ChatApiHandler = {} - @EditorRealTimeController = - emitToRoom:sinon.stub() - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user_id) - @ChatController = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "logger-sharelatex": log: -> - "./ChatApiHandler": @ChatApiHandler - "../Editor/EditorRealTimeController": @EditorRealTimeController - '../Authentication/AuthenticationController': @AuthenticationController - '../User/UserInfoManager': @UserInfoManager = {} - '../User/UserInfoController': @UserInfoController = {} - '../Comments/CommentsController': @CommentsController = {} - @req = - params: - project_id: @project_id - @res = - json: sinon.stub() - send: sinon.stub() - - describe "sendMessage", -> - beforeEach -> - @req.body = - content: @content = "message-content" - @UserInfoManager.getPersonalInfo = sinon.stub().yields(null, @user = {"unformatted": "user"}) - @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formatted_user = {"formatted": "user"}) - @ChatApiHandler.sendGlobalMessage = sinon.stub().yields(null, @message = {"mock": "message", user_id: @user_id}) - @ChatController.sendMessage @req, @res - - it "should look up the user", -> - @UserInfoManager.getPersonalInfo - .calledWith(@user_id) - .should.equal true - - it "should format and inject the user into the message", -> - @UserInfoController.formatPersonalInfo - .calledWith(@user) - .should.equal true - @message.user.should.deep.equal @formatted_user - - it "should tell the chat handler about the message", -> - @ChatApiHandler.sendGlobalMessage - .calledWith(@project_id, @user_id, @content) - .should.equal true - - it "should tell the editor real time controller about the update with the data from the chat handler", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "new-chat-message", @message) - .should.equal true - - it "should return a 204 status code", -> - @res.send.calledWith(204).should.equal true - - describe "getMessages", -> - beforeEach -> - @req.query = - limit: @limit = "30" - before: @before = "12345" - @ChatController._injectUserInfoIntoThreads = sinon.stub().yields() - @ChatApiHandler.getGlobalMessages = sinon.stub().yields(null, @messages = ["mock", "messages"]) - @ChatController.getMessages @req, @res - - it "should ask the chat handler about the request", -> - @ChatApiHandler.getGlobalMessages - .calledWith(@project_id, @limit, @before) - .should.equal true - - it "should return the messages", -> - @res.json.calledWith(@messages).should.equal true - - describe "_injectUserInfoIntoThreads", -> - beforeEach -> - @users = { - "user_id_1": { - "mock": "user_1" - } - "user_id_2": { - "mock": "user_2" - } - } - @UserInfoManager.getPersonalInfo = (user_id, callback) => - return callback(null, @users[user_id]) - sinon.spy @UserInfoManager, "getPersonalInfo" - @UserInfoController.formatPersonalInfo = (user) -> - return { "formatted": user["mock"] } - - it "should inject a user object into messaged and resolved data", (done) -> - @ChatController._injectUserInfoIntoThreads { - thread1: { - resolved: true - resolved_by_user_id: "user_id_1" - messages: [{ - user_id: "user_id_1" - content: "foo" - }, { - user_id: "user_id_2" - content: "bar" - }] - }, - thread2: { - messages: [{ - user_id: "user_id_1" - content: "baz" - }] - } - }, (error, threads) -> - expect(threads).to.deep.equal { - thread1: { - resolved: true - resolved_by_user_id: "user_id_1" - resolved_by_user: { "formatted": "user_1" } - messages: [{ - user_id: "user_id_1" - user: { "formatted": "user_1" } - content: "foo" - }, { - user_id: "user_id_2" - user: { "formatted": "user_2" } - content: "bar" - }] - }, - thread2: { - messages: [{ - user_id: "user_id_1" - user: { "formatted": "user_1" } - content: "baz" - }] - } - } - done() - - it "should only need to look up each user once", (done) -> - @ChatController._injectUserInfoIntoThreads [{ - messages: [{ - user_id: "user_id_1" - content: "foo" - }, { - user_id: "user_id_1" - content: "bar" - }] - }], (error, threads) => - @UserInfoManager.getPersonalInfo.calledOnce.should.equal true - done() \ No newline at end of file diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsControllerTests.coffee deleted file mode 100644 index 9a1be3dbb0..0000000000 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ /dev/null @@ -1,110 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsController.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -ObjectId = require("mongojs").ObjectId - -describe "CollaboratorsController", -> - beforeEach -> - @CollaboratorsController = SandboxedModule.require modulePath, requires: - "../Project/ProjectGetter": @ProjectGetter = {} - "./CollaboratorsHandler": @CollaboratorsHandler = {} - "../Editor/EditorRealTimeController": @EditorRealTimeController = {} - '../Subscription/LimitationsManager' : @LimitationsManager = {} - '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} - '../User/UserGetter': @UserGetter = {} - 'logger-sharelatex': @logger = {err: sinon.stub(), erro: sinon.stub(), log: sinon.stub()} - @res = new MockResponse() - @req = new MockRequest() - - @project_id = "project-id-123" - @callback = sinon.stub() - - describe "removeUserFromProject", -> - beforeEach -> - @req.params = - Project_id: @project_id = "project-id-123" - user_id: @user_id = "user-id-123" - @res.sendStatus = sinon.stub() - @EditorRealTimeController.emitToRoom = sinon.stub() - @CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) - @CollaboratorsController.removeUserFromProject @req, @res - - it "should from the user from the project", -> - @CollaboratorsHandler.removeUserFromProject - .calledWith(@project_id, @user_id) - .should.equal true - - it "should emit a userRemovedFromProject event to the proejct", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'userRemovedFromProject', @user_id) - .should.equal true - - it "should send the back a success response", -> - @res.sendStatus.calledWith(204).should.equal true - - it 'should have called emitToRoom', -> - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - - describe "removeSelfFromProject", -> - beforeEach -> - @req.session = - user: _id: @user_id = "user-id-123" - @req.params = Project_id: @project_id - @res.sendStatus = sinon.stub() - @EditorRealTimeController.emitToRoom = sinon.stub() - @CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) - @CollaboratorsController.removeSelfFromProject(@req, @res) - - it "should remove the logged in user from the project", -> - @CollaboratorsHandler.removeUserFromProject - .calledWith(@project_id, @user_id) - .should.equal true - - it "should emit a userRemovedFromProject event to the proejct", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'userRemovedFromProject', @user_id) - .should.equal true - - it "should return a success code", -> - @res.sendStatus.calledWith(204).should.equal true - - describe 'getAllMembers', -> - beforeEach -> - @req.session = - user: _id: @user_id = "user-id-123" - @req.params = Project_id: @project_id - @res.json = sinon.stub() - @next = sinon.stub() - @members = [{a: 1}] - @CollaboratorsHandler.getAllInvitedMembers = sinon.stub().callsArgWith(1, null, @members) - @CollaboratorsController.getAllMembers(@req, @res, @next) - - it 'should not produce an error', -> - @next.callCount.should.equal 0 - - it 'should produce a json response', -> - @res.json.callCount.should.equal 1 - @res.json.calledWith({members: @members}).should.equal true - - it 'should call CollaboratorsHandler.getAllMembers', -> - @CollaboratorsHandler.getAllInvitedMembers.callCount.should.equal 1 - - describe 'when CollaboratorsHandler.getAllInvitedMembers produces an error', -> - beforeEach -> - @res.json = sinon.stub() - @next = sinon.stub() - @CollaboratorsHandler.getAllInvitedMembers = sinon.stub().callsArgWith(1, new Error('woops')) - @CollaboratorsController.getAllMembers(@req, @res, @next) - - it 'should produce an error', -> - @next.callCount.should.equal 1 - @next.firstCall.args[0].should.be.instanceof Error - - it 'should not produce a json response', -> - @res.json.callCount.should.equal 0 diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsHandlerTests.coffee deleted file mode 100644 index 4a2c33a2aa..0000000000 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ /dev/null @@ -1,541 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Collaborators/CollaboratorsHandler" -expect = require("chai").expect -Errors = require "../../../../app/js/Features/Errors/Errors.js" -ObjectId = require('mongojs').ObjectId - -describe "CollaboratorsHandler", -> - beforeEach -> - @CollaboratorHandler = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = { log: sinon.stub(), err: sinon.stub() } - '../User/UserCreator': @UserCreator = {} - '../User/UserGetter': @UserGetter = {} - "../Contacts/ContactManager": @ContactManager = {} - "../../models/Project": Project: @Project = {} - "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} - "../Project/ProjectGetter": @ProjectGetter = {} - "./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {} - "../Errors/Errors": Errors - "../Project/ProjectEditorHandler": @ProjectEditorHandler = {} - - @project_id = "mock-project-id" - @user_id = "mock-user-id" - @adding_user_id = "adding-user-id" - @email = "joe@sharelatex.com" - @callback = sinon.stub() - - describe "getMemberIdsWithPrivilegeLevels", -> - describe "with project", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub() - @ProjectGetter.getProject.withArgs( - @project_id, - {owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1, - tokenAccessReadOnly_refs: 1, tokenAccessReadAndWrite_refs: 1, publicAccesLevel: 1} - ).yields(null, @project = { - owner_ref: [ "owner-ref" ] - readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] - collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] - }) - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, @callback - - it "should return an array of member ids with their privilege levels", -> - @callback - .calledWith(null, [ - { id: "owner-ref", privilegeLevel: "owner", source: 'owner'} - { id: "read-write-ref-1", privilegeLevel: "readAndWrite", source: 'invite'} - { id: "read-write-ref-2", privilegeLevel: "readAndWrite", source: 'invite' } - { id: "read-only-ref-1", privilegeLevel: "readOnly", source: 'invite'} - { id: "read-only-ref-2", privilegeLevel: "readOnly", source: 'invite'} - ]) - .should.equal true - - describe "with a missing project", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub().yields(null, null) - - it "should return a NotFoundError", (done) -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, (error) -> - error.should.be.instanceof Errors.NotFoundError - done() - - describe "getMemberIds", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(@project_id) - .yields(null, [{id: "member-id-1", source: 'invite'}, {id: "member-id-2", source: 'token'}]) - @CollaboratorHandler.getMemberIds @project_id, @callback - - it "should return the ids", -> - @callback - .calledWith(null, ["member-id-1", "member-id-2"]) - .should.equal true - - describe "getInvitedMemberIds", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(@project_id) - .yields(null, [{id: "member-id-1", source: 'invite'}, {id: "member-id-2", source: 'token'}]) - @CollaboratorHandler.getInvitedMemberIds @project_id, @callback - - it "should return the invited ids", -> - @callback - .calledWith(null, ["member-id-1"]) - .should.equal true - - describe "getMembersWithPrivilegeLevels", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ - { id: "read-only-ref-1", privilegeLevel: "readOnly", source: 'token' } - { id: "read-only-ref-2", privilegeLevel: "readOnly", source: 'invite' } - { id: "read-write-ref-1", privilegeLevel: "readAndWrite", source: 'token' } - { id: "read-write-ref-2", privilegeLevel: "readAndWrite", source: 'invite' } - { id: "doesnt-exist", privilegeLevel: "readAndWrite", source: 'invite' } - ]) - @UserGetter.getUserOrUserStubById = sinon.stub() - @UserGetter.getUserOrUserStubById.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" }) - @UserGetter.getUserOrUserStubById.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" }) - @UserGetter.getUserOrUserStubById.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" }) - @UserGetter.getUserOrUserStubById.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" }) - @UserGetter.getUserOrUserStubById.withArgs("doesnt-exist").yields(null, null) - @CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback - - it "should return an array of members with their privilege levels", -> - @callback - .calledWith(null, [ - { user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" } - { user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" } - { user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" } - { user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" } - ]) - .should.equal true - - describe "getInvitedMembersWithPrivilegeLevels", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ - { id: "read-only-ref-1", privilegeLevel: "readOnly", source: 'token' } - { id: "read-only-ref-2", privilegeLevel: "readOnly", source: 'invite' } - { id: "read-write-ref-1", privilegeLevel: "readAndWrite", source: 'token' } - { id: "read-write-ref-2", privilegeLevel: "readAndWrite", source: 'invite' } - { id: "doesnt-exist", privilegeLevel: "readAndWrite", source: 'invite' } - ]) - @UserGetter.getUserOrUserStubById = sinon.stub() - @UserGetter.getUserOrUserStubById.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" }) - @UserGetter.getUserOrUserStubById.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" }) - @UserGetter.getUserOrUserStubById.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" }) - @UserGetter.getUserOrUserStubById.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" }) - @UserGetter.getUserOrUserStubById.withArgs("doesnt-exist").yields(null, null) - @CollaboratorHandler.getInvitedMembersWithPrivilegeLevels @project_id, @callback - - it "should return an array of invited members with their privilege levels", -> - @callback - .calledWith(null, [ - { user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" } - { user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" } - ]) - .should.equal true - - describe "getTokenMembersWithPrivilegeLevels", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ - { id: "read-only-ref-1", privilegeLevel: "readOnly", source: 'token' } - { id: "read-only-ref-2", privilegeLevel: "readOnly", source: 'invite' } - { id: "read-write-ref-1", privilegeLevel: "readAndWrite", source: 'token' } - { id: "read-write-ref-2", privilegeLevel: "readAndWrite", source: 'invite' } - { id: "doesnt-exist", privilegeLevel: "readAndWrite", source: 'invite' } - ]) - @UserGetter.getUserOrUserStubById = sinon.stub() - @UserGetter.getUserOrUserStubById.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" }) - @UserGetter.getUserOrUserStubById.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" }) - @UserGetter.getUserOrUserStubById.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" }) - @UserGetter.getUserOrUserStubById.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" }) - @UserGetter.getUserOrUserStubById.withArgs("doesnt-exist").yields(null, null) - @CollaboratorHandler.getTokenMembersWithPrivilegeLevels @project_id, @callback - - it "should return an array of token members with their privilege levels", -> - @callback - .calledWith(null, [ - { user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" } - { user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite"} - ]) - .should.equal true - - describe "getMemberIdPrivilegeLevel", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels - .withArgs(@project_id) - .yields(null, [ - {id: "member-id-1", privilegeLevel: "readAndWrite"} - {id: "member-id-2", privilegeLevel: "readOnly"} - ]) - - it "should return the privilege level if it exists", (done) -> - @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-2", @project_id, (error, level) -> - expect(level).to.equal "readOnly" - done() - - it "should return false if the member has no privilege level", (done) -> - @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) -> - expect(level).to.equal false - done() - - describe "isUserInvitedMemberOfProject", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() - - describe "when user is a member of the project", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ - { id: "not-the-user", privilegeLevel: "readOnly", source: 'invite' } - { id: @user_id, privilegeLevel: "readAndWrite", source: 'invite' } - ]) - @CollaboratorHandler.isUserInvitedMemberOfProject @user_id, @project_id, @callback - - it "should return true and the privilegeLevel", -> - @callback - .calledWith(null, true, "readAndWrite") - .should.equal true - - describe "when user is not a member of the project", -> - beforeEach -> - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ - { id: "not-the-user", privilegeLevel: "readOnly" } - ]) - @CollaboratorHandler.isUserInvitedMemberOfProject @user_id, @project_id, @callback - - it "should return false", -> - @callback - .calledWith(null, false, null) - .should.equal true - - describe "getProjectsUserIsMemberOf", -> - beforeEach -> - @fields = "mock fields" - @Project.find = sinon.stub() - @Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"]) - @Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"]) - @Project.find.withArgs({tokenAccessReadAndWrite_refs:@user_id, publicAccesLevel: 'tokenBased'}, @fields).yields(null, ["mock-token-read-write-project-1", "mock-token-read-write-project-2"]) - @Project.find.withArgs({tokenAccessReadOnly_refs:@user_id, publicAccesLevel: 'tokenBased'}, @fields).yields(null, ["mock-token-read-only-project-1", "mock-token-read-only-project-2"]) - @CollaboratorHandler.getProjectsUserIsMemberOf @user_id, @fields, @callback - - it "should call the callback with the projects", -> - @callback - .calledWith( - null, - { - readAndWrite: ["mock-read-write-project-1", "mock-read-write-project-2"], - readOnly: ["mock-read-only-project-1", "mock-read-only-project-2"], - tokenReadAndWrite: ["mock-token-read-write-project-1", "mock-token-read-write-project-2"], - tokenReadOnly: ["mock-token-read-only-project-1", "mock-token-read-only-project-2"] - } - ) - .should.equal true - - describe "removeUserFromProject", -> - beforeEach -> - @Project.update = sinon.stub().callsArg(2) - @CollaboratorHandler.removeUserFromProject @project_id, @user_id, @callback - - it "should remove the user from mongo", -> - @Project.update - .calledWith({ - _id: @project_id - }, { - "$pull":{ - collaberator_refs:@user_id, readOnly_refs:@user_id, - tokenAccessReadOnly_refs:@user_id, tokenAccessReadAndWrite_refs:@user_id - } - }) - .should.equal true - - describe "addUserToProject", -> - beforeEach -> - @Project.update = sinon.stub().callsArg(2) - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project = {}) - @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArg(1) - @CollaboratorHandler.addEmailToProject = sinon.stub().callsArgWith(4, null, @user_id) - @ContactManager.addContact = sinon.stub() - - describe "as readOnly", -> - beforeEach -> - @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readOnly", @callback - - it "should add the user to the readOnly_refs", -> - @Project.update - .calledWith({ - _id: @project_id - }, { - "$addToSet":{ readOnly_refs: @user_id } - }) - .should.equal true - - it "should flush the project to the TPDS", -> - @ProjectEntityHandler.flushProjectToThirdPartyDataStore - .calledWith(@project_id) - .should.equal true - - it "should add the user as a contact for the adding user", -> - @ContactManager.addContact - .calledWith(@adding_user_id, @user_id) - .should.equal true - - describe "as readAndWrite", -> - beforeEach -> - @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback - - it "should add the user to the collaberator_refs", -> - @Project.update - .calledWith({ - _id: @project_id - }, { - "$addToSet":{ collaberator_refs: @user_id } - }) - .should.equal true - - it "should flush the project to the TPDS", -> - @ProjectEntityHandler.flushProjectToThirdPartyDataStore - .calledWith(@project_id) - .should.equal true - - describe "with invalid privilegeLevel", -> - beforeEach -> - @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "notValid", @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error()).should.equal true - - describe "when user already exists as a collaborator", -> - beforeEach -> - @project.collaberator_refs = [@user_id] - @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback - - it "should not add the user again", -> - @Project.update.called.should.equal false - - describe "with null adding_user_id", -> - beforeEach -> - @CollaboratorHandler.addUserIdToProject @project_id, null, @user_id, "readAndWrite", @callback - - it "should not add the adding user as a contact", -> - @ContactManager.addContact.called.should.equal(false) - - describe "removeUserFromAllProjects", -> - beforeEach (done) -> - @CollaboratorHandler.getProjectsUserIsMemberOf = sinon.stub() - @CollaboratorHandler.getProjectsUserIsMemberOf.withArgs(@user_id, { _id: 1 }).yields( - null, - { - readAndWrite: [ { _id: "read-and-write-0" }, { _id: "read-and-write-1" }, null ], - readOnly: [ { _id: "read-only-0" }, { _id: "read-only-1" }, null ] - tokenReadAndWrite: [ { _id: "token-read-and-write-0" }, { _id: "token-read-and-write-1" }, null ] - tokenReadOnly: [ { _id: "token-read-only-0" }, { _id: "token-read-only-1" }, null ] - } - ) - @CollaboratorHandler.removeUserFromProject = sinon.stub().yields() - @CollaboratorHandler.removeUserFromAllProjets @user_id, done - - it "should remove the user from each project", -> - expectedProjects = [ - "read-and-write-0", "read-and-write-1", - "read-only-0", "read-only-1", - "token-read-and-write-0", "token-read-and-write-1", - "token-read-only-0", "token-read-only-1", - ] - for project_id in expectedProjects - @CollaboratorHandler.removeUserFromProject - .calledWith(project_id, @user_id) - .should.equal true - - describe 'getAllInvitedMembers', -> - - beforeEach -> - @owning_user = {_id: 'owner-id', email: 'owner@example.com', features: {a: 1}} - @readwrite_user = {_id: 'readwrite-id', email: 'readwrite@example.com'} - @members = [ - {user: @owning_user, privilegeLevel: "owner"}, - {user: @readwrite_user, privilegeLevel: "readAndWrite"} - ] - @CollaboratorHandler.getInvitedMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members) - @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = { - owner: @owning_user, - ownerFeatures: @owning_user.features, - members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ] - }) - @callback = sinon.stub() - @CollaboratorHandler.getAllInvitedMembers @project_id, @callback - - it 'should not produce an error', -> - @callback.callCount.should.equal 1 - expect(@callback.firstCall.args[0]).to.equal null - - it 'should produce a list of members', -> - @callback.callCount.should.equal 1 - expect(@callback.firstCall.args[1]).to.deep.equal @views.members - - it 'should call getMembersWithPrivileges', -> - @CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.callCount.should.equal 1 - @CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id - - it 'should call ProjectEditorHandler.buildOwnerAndMembersViews', -> - @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 1 - @ProjectEditorHandler.buildOwnerAndMembersViews.firstCall.args[0].should.equal @members - - describe 'when getMembersWithPrivileges produces an error', -> - - beforeEach -> - @CollaboratorHandler.getInvitedMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, new Error('woops')) - @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = { - owner: @owning_user, - ownerFeatures: @owning_user.features, - members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ] - }) - @callback = sinon.stub() - @CollaboratorHandler.getAllInvitedMembers @project_id, @callback - - it 'should produce an error', -> - @callback.callCount.should.equal 1 - expect(@callback.firstCall.args[0]).to.not.equal null - expect(@callback.firstCall.args[0]).to.be.instanceof Error - - it 'should call getMembersWithPrivileges', -> - @CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.callCount.should.equal 1 - @CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id - - it 'should not call ProjectEditorHandler.buildOwnerAndMembersViews', -> - @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 0 - - describe 'userIsTokenMember', -> - beforeEach -> - @user_id = ObjectId() - @project_id = ObjectId() - @project = {_id: @project_id} - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - - it 'should check the database', (done) -> - @CollaboratorHandler.userIsTokenMember @user_id, @project_id, (err, isTokenMember) => - @Project.findOne.callCount.should.equal 1 - done() - - it 'should return true when the project is found', (done) -> - @CollaboratorHandler.userIsTokenMember @user_id, @project_id, (err, isTokenMember) => - expect(err).to.not.exist - expect(isTokenMember).to.equal true - done() - - it 'should return false when the project is not found', (done) -> - @project = null - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - @CollaboratorHandler.userIsTokenMember @user_id, @project_id, (err, isTokenMember) => - expect(err).to.not.exist - expect(isTokenMember).to.equal false - done() - - describe 'transferProjects', -> - beforeEach -> - @from_user_id = "from-user-id" - @to_user_id = "to-user-id" - @projects = [{ - _id: "project-id-1" - }, { - _id: "project-id-2" - }] - @Project.find = sinon.stub().yields(null, @projects) - @Project.update = sinon.stub().yields() - @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().yields() - - describe "successfully", -> - beforeEach -> - @CollaboratorHandler.transferProjects @from_user_id, @to_user_id, @callback - - it "should look up the affected projects", -> - @Project.find - .calledWith({ - $or : [ - { owner_ref: @from_user_id } - { collaberator_refs: @from_user_id } - { readOnly_refs: @from_user_id } - ] - }) - .should.equal true - - it "should transfer owned projects", -> - @Project.update - .calledWith({ - owner_ref: @from_user_id - }, { - $set: { owner_ref: @to_user_id } - }, { - multi: true - }) - .should.equal true - - it "should transfer collaborator projects", -> - @Project.update - .calledWith({ - collaberator_refs: @from_user_id - }, { - $addToSet: { collaberator_refs: @to_user_id } - }, { - multi: true - }) - .should.equal true - @Project.update - .calledWith({ - collaberator_refs: @from_user_id - }, { - $pull: { collaberator_refs: @from_user_id } - }, { - multi: true - }) - .should.equal true - - it "should transfer read only collaborator projects", -> - @Project.update - .calledWith({ - readOnly_refs: @from_user_id - }, { - $addToSet: { readOnly_refs: @to_user_id } - }, { - multi: true - }) - .should.equal true - @Project.update - .calledWith({ - readOnly_refs: @from_user_id - }, { - $pull: { readOnly_refs: @from_user_id } - }, { - multi: true - }) - .should.equal true - - it "should flush each project to the TPDS", -> - for project in @projects - @ProjectEntityHandler.flushProjectToThirdPartyDataStore - .calledWith(project._id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when flushing to TPDS fails", -> - beforeEach -> - @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().yields(new Error('oops')) - @CollaboratorHandler.transferProjects @from_user_id, @to_user_id, @callback - - it "should log an error", -> - @logger.err.called.should.equal true - - it "should not return an error since it happens in the background", -> - @callback.called.should.equal true - @callback.calledWith(new Error('oops')).should.equal false diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee deleted file mode 100644 index 69fff44516..0000000000 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ /dev/null @@ -1,801 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteController.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -ObjectId = require("mongojs").ObjectId - -describe "CollaboratorsInviteController", -> - beforeEach -> - @user = - _id: 'id' - @AnalyticsManger = recordEvent: sinon.stub() - @sendingUser = null - @AuthenticationController = - getSessionUser: (req) => - @sendingUser = req.session.user - return @sendingUser - - @RateLimiter = - addCount: sinon.stub - - @LimitationsManager = {} - @UserGetter = - getUserByAnyEmail: sinon.stub() - getUser: sinon.stub() - - @CollaboratorsInviteController = SandboxedModule.require modulePath, requires: - "../Project/ProjectGetter": @ProjectGetter = {} - '../Subscription/LimitationsManager' : @LimitationsManager - '../User/UserGetter': @UserGetter - "./CollaboratorsHandler": @CollaboratorsHandler = {} - "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} - 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} - "../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()} - "../Notifications/NotificationsBuilder": @NotificationsBuilder = {} - "../Analytics/AnalyticsManager": @AnalyticsManger - '../Authentication/AuthenticationController': @AuthenticationController - 'settings-sharelatex': @settings = {} - "../../infrastructure/RateLimiter":@RateLimiter - @res = new MockResponse() - @req = new MockRequest() - - @project_id = "project-id-123" - @callback = sinon.stub() - - describe 'getAllInvites', -> - - beforeEach -> - @fakeInvites = [ - {_id: ObjectId(), one: 1}, - {_id: ObjectId(), two: 2} - ] - @req.params = - Project_id: @project_id - @res.json = sinon.stub() - @next = sinon.stub() - - describe 'when all goes well', -> - - beforeEach -> - @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @fakeInvites) - @CollaboratorsInviteController.getAllInvites @req, @res, @next - - it 'should not produce an error', -> - @next.callCount.should.equal 0 - - it 'should produce a list of invite objects', -> - @res.json.callCount.should.equal 1 - @res.json.calledWith({invites: @fakeInvites}).should.equal true - - it 'should have called CollaboratorsInviteHandler.getAllInvites', -> - @CollaboratorsInviteHandler.getAllInvites.callCount.should.equal 1 - @CollaboratorsInviteHandler.getAllInvites.calledWith(@project_id).should.equal true - - describe 'when CollaboratorsInviteHandler.getAllInvites produces an error', -> - - beforeEach -> - @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, new Error('woops')) - @CollaboratorsInviteController.getAllInvites @req, @res, @next - - it 'should produce an error', -> - @next.callCount.should.equal 1 - @next.firstCall.args[0].should.be.instanceof Error - - describe 'inviteToProject', -> - - beforeEach -> - @targetEmail = "user@example.com" - @req.params = - Project_id: @project_id - @current_user = - _id: @current_user_id = "current-user-id" - @req.session = - user: @current_user - @req.body = - email: @targetEmail - privileges: @privileges = "readAndWrite" - @res.json = sinon.stub() - @res.sendStatus = sinon.stub() - @invite = { - _id: ObjectId(), - token: "htnseuthaouse", - sendingUserId: @current_user_id, - projectId: @targetEmail, - targetEmail: 'user@example.com' - createdAt: new Date(), - } - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) - @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite) - @err = new Error('woops') - @callback = sinon.stub() - @next = sinon.stub() - - describe 'when all goes well', -> - - beforeEach -> - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it 'should produce json response', -> - @res.json.callCount.should.equal 1 - ({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0]) - - it 'should have called canAddXCollaborators', -> - @LimitationsManager.canAddXCollaborators.callCount.should.equal 1 - @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true - - it 'should have called _checkShouldInviteEmail', -> - @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 - @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true - - it 'should have called inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 - @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true - - it 'should have called emitToRoom', -> - @EditorRealTimeController.emitToRoom.callCount.should.equal 1 - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - - describe 'when the user is not allowed to add more collaborators', -> - - beforeEach -> - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it 'should produce json response without an invite', -> - @res.json.callCount.should.equal 1 - ({invite: null}).should.deep.equal(@res.json.firstCall.args[0]) - - it 'should not have called _checkShouldInviteEmail', -> - @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 - @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false - - it 'should not have called inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 - - describe 'when canAddXCollaborators produces an error', -> - - beforeEach -> - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it 'should call next with an error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should not have called _checkShouldInviteEmail', -> - @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 - @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false - - it 'should not have called inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 - - describe 'when inviteToProject produces an error', -> - - beforeEach -> - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @err = new Error('woops') - @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it 'should call next with an error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should have called canAddXCollaborators', -> - @LimitationsManager.canAddXCollaborators.callCount.should.equal 1 - @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true - - it 'should have called _checkShouldInviteEmail', -> - @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 - @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true - - it 'should have called inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 - @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true - - describe 'when _checkShouldInviteEmail disallows the invite', -> - - beforeEach -> - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, false) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it 'should produce json response with no invite, and an error property', -> - @res.json.callCount.should.equal 1 - ({invite: null, error: 'cannot_invite_non_user'}).should.deep.equal(@res.json.firstCall.args[0]) - - it 'should have called _checkShouldInviteEmail', -> - @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 - @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true - - it 'should not have called inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 - - describe 'when _checkShouldInviteEmail produces an error', -> - - beforeEach -> - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, new Error('woops')) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it 'should call next with an error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should have called _checkShouldInviteEmail', -> - @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 - @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true - - it 'should not have called inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 - - describe 'when the user invites themselves to the project', -> - - beforeEach -> - @req.session.user = {_id: 'abc', email: 'me@example.com'} - @req.body.email = 'me@example.com' - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - - it 'should reject action, return json response with error code', -> - @res.json.callCount.should.equal 1 - ({invite: null, error: 'cannot_invite_self'}).should.deep.equal(@res.json.firstCall.args[0]) - - it 'should not have called canAddXCollaborators', -> - @LimitationsManager.canAddXCollaborators.callCount.should.equal 0 - - it 'should not have called _checkShouldInviteEmail', -> - @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 - - it 'should not have called inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 - - it 'should not have called emitToRoom', -> - @EditorRealTimeController.emitToRoom.callCount.should.equal 0 - - describe 'when _checkRateLimit returns false', -> - - beforeEach -> - @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, false) - @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) - @CollaboratorsInviteController.inviteToProject @req, @res, @next - - it 'should send a 429 response', -> - @res.sendStatus.calledWith(429).should.equal true - - it 'should not call inviteToProject', -> - @CollaboratorsInviteHandler.inviteToProject.called.should.equal false - - it 'should not call emitToRoom', -> - @EditorRealTimeController.emitToRoom.called.should.equal false - - describe "viewInvite", -> - - beforeEach -> - @token = "some-opaque-token" - @req.params = - Project_id: @project_id - token: @token - @req.session = - user: _id: @current_user_id = "current-user-id" - @res.render = sinon.stub() - @res.redirect = sinon.stub() - @res.sendStatus = sinon.stub() - @invite = { - _id: ObjectId(), - token: @token, - sendingUserId: ObjectId(), - projectId: @project_id, - targetEmail: 'user@example.com' - createdAt: new Date(), - } - @fakeProject = - _id: @project_id - name: "some project" - owner_ref: @invite.sendingUserId - collaberator_refs: [] - readOnly_refs: [] - @owner = - _id: @fakeProject.owner_ref - first_name: "John" - last_name: "Doe" - email: "john@example.com" - - @CollaboratorsHandler.isUserInvitedMemberOfProject = sinon.stub().callsArgWith(2, null, false, null) - @CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite) - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) - @UserGetter.getUser.callsArgWith(2, null, @owner) - - @callback = sinon.stub() - @next = sinon.stub() - - describe 'when the token is valid', -> - - beforeEach -> - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should render the view template', -> - @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite/show').should.equal true - - it 'should not call next', -> - @next.callCount.should.equal 0 - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - @CollaboratorsInviteHandler.getInviteByToken.calledWith(@fakeProject._id, @invite.token).should.equal true - - it 'should call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true - - it 'should call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 1 - @ProjectGetter.getProject.calledWith(@project_id).should.equal true - - describe 'when user is already a member of the project', -> - - beforeEach -> - @CollaboratorsHandler.isUserInvitedMemberOfProject = sinon.stub().callsArgWith(2, null, true, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should redirect to the project page', -> - @res.redirect.callCount.should.equal 1 - @res.redirect.calledWith("/project/#{@project_id}").should.equal true - - it 'should not call next with an error', -> - @next.callCount.should.equal 0 - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should not call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 - - it 'should not call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 0 - - it 'should not call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 0 - - describe 'when isUserInvitedMemberOfProject produces an error', -> - - beforeEach -> - @CollaboratorsHandler.isUserInvitedMemberOfProject = sinon.stub().callsArgWith(2, new Error('woops')) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should call next with an error', -> - @next.callCount.should.equal 1 - expect(@next.firstCall.args[0]).to.be.instanceof Error - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should not call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0 - - it 'should not call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 0 - - it 'should not call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 0 - - describe 'when the getInviteByToken produces an error', -> - - beforeEach -> - @err = new Error('woops') - @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should call next with the error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should not call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 0 - - it 'should not call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 0 - - describe 'when the getInviteByToken does not produce an invite', -> - - beforeEach -> - @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should render the not-valid view template', -> - @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite/not-valid').should.equal true - - it 'should not call next', -> - @next.callCount.should.equal 0 - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should not call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 0 - - it 'should not call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 0 - - describe 'when User.getUser produces an error', -> - - beforeEach -> - @UserGetter.getUser.callsArgWith(2, new Error('woops')) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should produce an error', -> - @next.callCount.should.equal 1 - expect(@next.firstCall.args[0]).to.be.instanceof Error - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - - it 'should call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true - - it 'should not call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 0 - - describe 'when User.getUser does not find a user', -> - - beforeEach -> - @UserGetter.getUser.callsArgWith(2, null, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should render the not-valid view template', -> - @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite/not-valid').should.equal true - - it 'should not call next', -> - @next.callCount.should.equal 0 - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - - it 'should call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true - - it 'should not call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 0 - - describe 'when getProject produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should produce an error', -> - @next.callCount.should.equal 1 - expect(@next.firstCall.args[0]).to.be.instanceof Error - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - - it 'should call User.getUser', -> - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true - - it 'should call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 1 - - describe 'when Project.getUser does not find a user', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, null) - @CollaboratorsInviteController.viewInvite @req, @res, @next - - it 'should render the not-valid view template', -> - @res.render.callCount.should.equal 1 - @res.render.calledWith('project/invite/not-valid').should.equal true - - it 'should not call next', -> - @next.callCount.should.equal 0 - - it 'should call CollaboratorsHandler.isUserInvitedMemberOfProject', -> - @CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal 1 - @CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true - - it 'should call getInviteByToken', -> - @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1 - - it 'should call getUser', -> - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true - - it 'should call ProjectGetter.getProject', -> - @ProjectGetter.getProject.callCount.should.equal 1 - - describe "resendInvite", -> - - beforeEach -> - @req.params = - Project_id: @project_id - invite_id: @invite_id = "thuseoautoh" - @req.session = - user: _id: @current_user_id = "current-user-id" - @res.render = sinon.stub() - @res.sendStatus = sinon.stub() - @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, null) - @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true) - @callback = sinon.stub() - @next = sinon.stub() - - describe 'when resendInvite does not produce an error', -> - - beforeEach -> - @CollaboratorsInviteController.resendInvite @req, @res, @next - - it 'should produce a 201 response', -> - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(201).should.equal true - - it 'should have called resendInvite', -> - @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1 - - it 'should check the rate limit', -> - @CollaboratorsInviteController._checkRateLimit.callCount.should.equal 1 - - describe 'when resendInvite produces an error', -> - - beforeEach -> - @err = new Error('woops') - @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, @err) - @CollaboratorsInviteController.resendInvite @req, @res, @next - - it 'should not produce a 201 response', -> - @res.sendStatus.callCount.should.equal 0 - - it 'should call next with the error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should have called resendInvite', -> - @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1 - - describe "revokeInvite", -> - - beforeEach -> - @req.params = - Project_id: @project_id - invite_id: @invite_id = "thuseoautoh" - @current_user = - _id: @current_user_id = "current-user-id" - @req.session = - user: @current_user - @res.render = sinon.stub() - @res.sendStatus = sinon.stub() - @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, null) - @callback = sinon.stub() - @next = sinon.stub() - - describe 'when revokeInvite does not produce an error', -> - - beforeEach -> - @CollaboratorsInviteController.revokeInvite @req, @res, @next - - it 'should produce a 201 response', -> - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(201).should.equal true - - it 'should have called revokeInvite', -> - @CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1 - - it 'should have called emitToRoom', -> - @EditorRealTimeController.emitToRoom.callCount.should.equal 1 - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - - describe 'when revokeInvite produces an error', -> - - beforeEach -> - @err = new Error('woops') - @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, @err) - @CollaboratorsInviteController.revokeInvite @req, @res, @next - - it 'should not produce a 201 response', -> - @res.sendStatus.callCount.should.equal 0 - - it 'should call next with the error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should have called revokeInvite', -> - @CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1 - - describe "acceptInvite", -> - - beforeEach -> - @req.params = - Project_id: @project_id - token: @token = "mock-token" - @req.session = - user: _id: @current_user_id = "current-user-id" - @res.render = sinon.stub() - @res.redirect = sinon.stub() - @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, null) - @callback = sinon.stub() - @next = sinon.stub() - - describe 'when acceptInvite does not produce an error', -> - - beforeEach -> - @CollaboratorsInviteController.acceptInvite @req, @res, @next - - it 'should redirect to project page', () -> - @res.redirect.callCount.should.equal 1 - @res.redirect.calledWith("/project/#{@project_id}").should.equal true - - it 'should have called acceptInvite', -> - @CollaboratorsInviteHandler.acceptInvite - .calledWith(@project_id, @token) - .should.equal true - - it 'should have called emitToRoom', -> - @EditorRealTimeController.emitToRoom.callCount.should.equal 1 - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true - - describe 'when revokeInvite produces an error', -> - - beforeEach -> - @err = new Error('woops') - @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, @err) - @CollaboratorsInviteController.acceptInvite @req, @res, @next - - it 'should not redirect to project page', -> - @res.redirect.callCount.should.equal 0 - - it 'should call next with the error', -> - @next.callCount.should.equal 1 - @next.calledWith(@err).should.equal true - - it 'should have called acceptInvite', -> - @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 - - describe '_checkShouldInviteEmail', -> - - beforeEach -> - @email = 'user@example.com' - - describe 'when we should be restricting to existing accounts', -> - - beforeEach -> - @settings.restrictInvitesToExistingAccounts = true - @call = (callback) => - @CollaboratorsInviteController._checkShouldInviteEmail @email, callback - - describe 'when user account is present', -> - - beforeEach -> - @user = {_id: ObjectId().toString()} - @UserGetter.getUserByAnyEmail = sinon.stub().callsArgWith(2, null, @user) - - it 'should callback with `true`', (done) -> - @call (err, shouldAllow) => - expect(err).to.equal null - expect(shouldAllow).to.equal true - done() - - describe 'when user account is absent', -> - - beforeEach -> - @user = null - @UserGetter.getUserByAnyEmail = sinon.stub().callsArgWith(2, null, @user) - - it 'should callback with `false`', (done) -> - @call (err, shouldAllow) => - expect(err).to.equal null - expect(shouldAllow).to.equal false - done() - - it 'should have called getUser', (done) -> - @call (err, shouldAllow) => - @UserGetter.getUserByAnyEmail.callCount.should.equal 1 - @UserGetter.getUserByAnyEmail.calledWith(@email, {_id: 1}).should.equal true - done() - - describe 'when getUser produces an error', -> - - beforeEach -> - @user = null - @UserGetter.getUserByAnyEmail = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should callback with an error', (done) -> - @call (err, shouldAllow) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(shouldAllow).to.equal undefined - done() - - describe '_checkRateLimit', -> - beforeEach -> - @settings.restrictInvitesToExistingAccounts = false - @sendingUserId = "32312313" - @LimitationsManager.allowedNumberOfCollaboratorsForUser = sinon.stub() - @LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null, 17) - - it 'should callback with `true` when rate limit under', (done) -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=> - @RateLimiter.addCount.called.should.equal true - result.should.equal true - done() - - it 'should callback with `false` when rate limit hit', (done) -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) - @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=> - @RateLimiter.addCount.called.should.equal true - result.should.equal false - done() - - it 'should call rate limiter with 10x the collaborators', (done) -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=> - @RateLimiter.addCount.args[0][0].throttle.should.equal(170) - done() - - it 'should call rate limiter with 200 when collaborators is -1', (done) -> - @LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null, -1) - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=> - @RateLimiter.addCount.args[0][0].throttle.should.equal(200) - done() - - it 'should call rate limiter with 10 when user has no collaborators set', (done) -> - @LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null) - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=> - @RateLimiter.addCount.args[0][0].throttle.should.equal(10) - done() diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee deleted file mode 100644 index edad39a637..0000000000 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ /dev/null @@ -1,723 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteHandler.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -ObjectId = require("mongojs").ObjectId -Crypto = require('crypto') - -describe "CollaboratorsInviteHandler", -> - beforeEach -> - @ProjectInvite = class ProjectInvite - constructor: (options={}) -> - this._id = ObjectId() - for k,v of options - this[k] = v - this - save: sinon.stub() - @findOne: sinon.stub() - @find: sinon.stub() - @remove: sinon.stub() - @count: sinon.stub() - @Crypto = Crypto - @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires: - 'settings-sharelatex': @settings = {} - '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite} - 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()} - './CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {} - "./CollaboratorsHandler": @CollaboratorsHandler = {addUserIdToProject: sinon.stub()} - '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} - "../Project/ProjectGetter": @ProjectGetter = {} - "../Notifications/NotificationsBuilder": @NotificationsBuilder = {} - 'crypto': @Crypto - - @projectId = ObjectId() - @sendingUserId = ObjectId() - @sendingUser = - _id: @sendingUserId - name: "Bob" - @email = "user@example.com" - @userId = ObjectId() - @user = - _id: @userId - email: 'someone@example.com' - @inviteId = ObjectId() - @token = 'hnhteaosuhtaeosuahs' - @privileges = "readAndWrite" - @fakeInvite = - _id: @inviteId - email: @email - token: @token - sendingUserId: @sendingUserId - projectId: @projectId - privileges: @privileges - createdAt: new Date() - - describe 'getInviteCount', -> - - beforeEach -> - @ProjectInvite.count.callsArgWith(1, null, 2) - @call = (callback) => - @CollaboratorsInviteHandler.getInviteCount @projectId, callback - - it 'should not produce an error', (done) -> - @call (err, invites) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should produce the count of documents', (done) -> - @call (err, count) => - expect(count).to.equal 2 - done() - - describe 'when model.count produces an error', -> - - beforeEach -> - @ProjectInvite.count.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, count) => - expect(err).to.be.instanceof Error - done() - - describe 'getAllInvites', -> - - beforeEach -> - @fakeInvites = [ - {_id: ObjectId(), one: 1}, - {_id: ObjectId(), two: 2} - ] - @ProjectInvite.find.callsArgWith(1, null, @fakeInvites) - @call = (callback) => - @CollaboratorsInviteHandler.getAllInvites @projectId, callback - - describe 'when all goes well', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err, invites) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should produce a list of invite objects', (done) -> - @call (err, invites) => - expect(invites).to.not.be.oneOf [null, undefined] - expect(invites).to.deep.equal @fakeInvites - done() - - it 'should have called ProjectInvite.find', (done) -> - @call (err, invites) => - @ProjectInvite.find.callCount.should.equal 1 - @ProjectInvite.find.calledWith({projectId: @projectId}).should.equal true - done() - - describe 'when ProjectInvite.find produces an error', -> - - beforeEach -> - @ProjectInvite.find.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, invites) => - expect(err).to.be.instanceof Error - done() - - describe 'inviteToProject', -> - - beforeEach -> - @ProjectInvite::save = sinon.spy (cb) -> cb(null, this) - @randomBytesSpy = sinon.spy(@Crypto, 'randomBytes') - @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null) - @call = (callback) => - @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUser, @email, @privileges, callback - - afterEach -> - @randomBytesSpy.restore() - - describe 'when all goes well', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err, invite) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should produce the invite object', (done) -> - @call (err, invite) => - expect(invite).to.not.equal null - expect(invite).to.not.equal undefined - expect(invite).to.be.instanceof Object - expect(invite).to.have.all.keys ['_id', 'email', 'token', 'sendingUserId', 'projectId', 'privileges'] - done() - - it 'should have generated a random token', (done) -> - @call (err, invite) => - @randomBytesSpy.callCount.should.equal 1 - done() - - it 'should have called ProjectInvite.save', (done) -> - @call (err, invite) => - @ProjectInvite::save.callCount.should.equal 1 - done() - - it 'should have called _sendMessages', (done) -> - @call (err, invite) => - @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1 - @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser).should.equal true - done() - - describe 'when saving model produces an error', -> - - beforeEach -> - @ProjectInvite::save = sinon.spy (cb) -> cb(new Error('woops'), this) - - it 'should produce an error', (done) -> - @call (err, invite) => - expect(err).to.be.instanceof Error - done() - - describe '_sendMessages', -> - - beforeEach -> - @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(4, null) - @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null) - @call = (callback) => - @CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback - - describe 'when all goes well', -> - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) -> - @call (err) => - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1 - @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @fakeInvite.email, @fakeInvite).should.equal true - done() - - it 'should call _trySendInviteNotification', (done) -> - @call (err) => - @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 1 - @CollaboratorsInviteHandler._trySendInviteNotification.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true - done() - - describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', -> - - beforeEach -> - @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(4, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, invite) => - expect(err).to.be.instanceof Error - done() - - it 'should not call _trySendInviteNotification', (done) -> - @call (err) => - @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 0 - done() - - describe 'when _trySendInviteNotification produces an error', -> - - beforeEach -> - @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, invite) => - expect(err).to.be.instanceof Error - done() - - describe 'revokeInvite', -> - - beforeEach -> - @ProjectInvite.remove.callsArgWith(1, null) - @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null) - @call = (callback) => - @CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback - - describe 'when all goes well', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 1 - @ProjectInvite.remove.calledWith({projectId: @projectId, _id: @inviteId}).should.equal true - done() - - it 'should call _tryCancelInviteNotification', (done) -> - @call (err) => - @CollaboratorsInviteHandler._tryCancelInviteNotification.callCount.should.equal 1 - @CollaboratorsInviteHandler._tryCancelInviteNotification.calledWith(@inviteId).should.equal true - done() - - describe 'when remove produces an error', -> - - beforeEach -> - @ProjectInvite.remove.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe 'resendInvite', -> - - beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) - @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null) - @call = (callback) => - @CollaboratorsInviteHandler.resendInvite @projectId, @sendingUser, @inviteId, callback - - describe 'when all goes well', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call ProjectInvite.findOne', (done) -> - @call (err, invite) => - @ProjectInvite.findOne.callCount.should.equal 1 - @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId}).should.equal true - done() - - it 'should have called _sendMessages', (done) -> - @call (err, invite) => - @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1 - @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true - done() - - describe 'when findOne produces an error', -> - - beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, invite) => - expect(err).to.be.instanceof Error - done() - - it 'should not have called _sendMessages', (done) -> - @call (err, invite) => - @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0 - done() - - describe 'when findOne does not find an invite', -> - - beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, null, null) - - it 'should not produce an error', (done) -> - @call (err, invite) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should not have called _sendMessages', (done) -> - @call (err, invite) => - @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0 - done() - - describe 'getInviteByToken', -> - - beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite) - @call = (callback) => - @CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback - - describe 'when all goes well', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err, invite) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should produce the invite object', (done) -> - @call (err, invite) => - expect(invite).to.deep.equal @fakeInvite - done() - - it 'should call ProjectInvite.findOne', (done) -> - @call (err, invite) => - @ProjectInvite.findOne.callCount.should.equal 1 - @ProjectInvite.findOne.calledWith({projectId: @projectId, token: @token}).should.equal true - done() - - describe 'when findOne produces an error', -> - - beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, invite) => - expect(err).to.be.instanceof Error - done() - - describe 'when findOne does not find an invite', -> - - beforeEach -> - @ProjectInvite.findOne.callsArgWith(1, null, null) - - it 'should not produce an error', (done) -> - @call (err, invite) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should not produce an invite object', (done) -> - @call (err, invite) => - expect(invite).to.not.be.instanceof Error - expect(invite).to.be.oneOf [null, undefined] - done() - - describe 'acceptInvite', -> - - beforeEach -> - @fakeProject = - _id: @projectId - collaberator_refs: [] - readOnly_refs: [] - @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, null) - @_getInviteByToken = sinon.stub(@CollaboratorsInviteHandler, 'getInviteByToken') - @_getInviteByToken.callsArgWith(2, null, @fakeInvite) - @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null) - @ProjectInvite.remove.callsArgWith(1, null) - @call = (callback) => - @CollaboratorsInviteHandler.acceptInvite @projectId, @token, @user, callback - - afterEach -> - @_getInviteByToken.restore() - - describe 'when all goes well', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should have called getInviteByToken', (done) -> - @call (err) => - @_getInviteByToken.callCount.should.equal 1 - @_getInviteByToken.calledWith(@projectId, @token).should.equal true - done() - - it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> - @call (err) => - @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 - @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true - done() - - it 'should have called ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 1 - @ProjectInvite.remove.calledWith({_id: @inviteId}).should.equal true - done() - - describe 'when the invite is for readOnly access', -> - - beforeEach -> - @fakeInvite.privileges = 'readOnly' - @_getInviteByToken.callsArgWith(2, null, @fakeInvite) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> - @call (err) => - @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 - @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true - done() - - describe 'when getInviteByToken does not find an invite', -> - - beforeEach -> - @_getInviteByToken.callsArgWith(2, null, null) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - expect(err.name).to.equal "NotFoundError" - done() - - it 'should have called getInviteByToken', (done) -> - @call (err) => - @_getInviteByToken.callCount.should.equal 1 - @_getInviteByToken.calledWith(@projectId, @token).should.equal true - done() - - it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) -> - @call (err) => - @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0 - done() - - it 'should not have called ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 0 - done() - - describe 'when getInviteByToken produces an error', -> - - beforeEach -> - @_getInviteByToken.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should have called getInviteByToken', (done) -> - @call (err) => - @_getInviteByToken.callCount.should.equal 1 - @_getInviteByToken.calledWith(@projectId, @token).should.equal true - done() - - it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) -> - @call (err) => - @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0 - done() - - it 'should not have called ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 0 - done() - - describe 'when addUserIdToProject produces an error', -> - - beforeEach -> - @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should have called getInviteByToken', (done) -> - @call (err) => - @_getInviteByToken.callCount.should.equal 1 - @_getInviteByToken.calledWith(@projectId, @token).should.equal true - done() - - it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> - @call (err) => - @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 - @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true - done() - - it 'should not have called ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 0 - done() - - describe 'when ProjectInvite.remove produces an error', -> - - beforeEach -> - @ProjectInvite.remove.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should have called getInviteByToken', (done) -> - @call (err) => - @_getInviteByToken.callCount.should.equal 1 - @_getInviteByToken.calledWith(@projectId, @token).should.equal true - done() - - it 'should have called CollaboratorsHandler.addUserIdToProject', (done) -> - @call (err) => - @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1 - @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true - done() - - it 'should have called ProjectInvite.remove', (done) -> - @call (err) => - @ProjectInvite.remove.callCount.should.equal 1 - done() - - describe '_tryCancelInviteNotification', -> - beforeEach -> - @inviteId = ObjectId() - @currentUser = {_id: ObjectId()} - @notification = {read: sinon.stub().callsArgWith(0, null)} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - @call = (callback) => - @CollaboratorsInviteHandler._tryCancelInviteNotification @inviteId, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call notification.read', (done) -> - @call (err) => - @notification.read.callCount.should.equal 1 - done() - - describe 'when notification.read produces an error', -> - beforeEach -> - @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe "_trySendInviteNotification", -> - - beforeEach -> - @invite = - _id: ObjectId(), - token: "some_token", - sendingUserId: ObjectId(), - projectId: @project_id, - targetEmail: 'user@example.com' - createdAt: new Date(), - @sendingUser = - _id: ObjectId() - first_name: "jim" - @existingUser = {_id: ObjectId()} - @UserGetter.getUserByAnyEmail = sinon.stub().callsArgWith(2, null, @existingUser) - @fakeProject = - _id: @project_id - name: "some project" - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject) - @notification = {create: sinon.stub().callsArgWith(0, null)} - @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification) - @call = (callback) => - @CollaboratorsInviteHandler._trySendInviteNotification @project_id, @sendingUser, @invite, callback - - describe 'when the user exists', -> - - beforeEach -> - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUserByAnyEmail.callCount.should.equal 1 - @UserGetter.getUserByAnyEmail.calledWith(@invite.email).should.equal true - done() - - it 'should call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 1 - @ProjectGetter.getProject.calledWith(@project_id).should.equal true - done() - - it 'should call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 1 - @notification.create.callCount.should.equal 1 - done() - - describe 'when getProject produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - - describe 'when projectInvite.create produces an error', -> - - beforeEach -> - @notification.create.callsArgWith(0, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe 'when the user does not exist', -> - - beforeEach -> - @UserGetter.getUserByAnyEmail = sinon.stub().callsArgWith(2, null, null) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.be.oneOf [null, undefined] - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUserByAnyEmail.callCount.should.equal 1 - @UserGetter.getUserByAnyEmail.calledWith(@invite.email).should.equal true - done() - - it 'should not call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 0 - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() - - describe 'when the getUser produces an error', -> - - beforeEach -> - @UserGetter.getUserByAnyEmail = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should call getUser', (done) -> - @call (err) => - @UserGetter.getUserByAnyEmail.callCount.should.equal 1 - @UserGetter.getUserByAnyEmail.calledWith(@invite.email).should.equal true - done() - - it 'should not call getProject', (done) -> - @call (err) => - @ProjectGetter.getProject.callCount.should.equal 0 - done() - - it 'should not call NotificationsBuilder.projectInvite.create', (done) -> - @call (err) => - @NotificationsBuilder.projectInvite.callCount.should.equal 0 - @notification.create.callCount.should.equal 0 - done() diff --git a/services/web/test/unit/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/unit/coffee/Compile/ClsiCookieManagerTests.coffee deleted file mode 100644 index 42a8aac3bc..0000000000 --- a/services/web/test/unit/coffee/Compile/ClsiCookieManagerTests.coffee +++ /dev/null @@ -1,163 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -assert = chai.assert -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Compile/ClsiCookieManager.js" -SandboxedModule = require('sandboxed-module') -realRequst = require("request") - -describe "ClsiCookieManager", -> - beforeEach -> - self = @ - @redisMulti = - set:sinon.stub() - get:sinon.stub() - expire:sinon.stub() - exec:sinon.stub() - @redis = - auth:-> - get:sinon.stub() - multi: -> return self.redisMulti - @project_id = "123423431321" - @request = - get: sinon.stub() - cookie:realRequst.cookie - jar: realRequst.jar - @settings = - redis: - web:"redis.something" - apis: - clsi: - url: "http://clsi.example.com" - clsiCookie: - ttl:Math.random() - key: "coooookie" - @requires = - "../../infrastructure/RedisWrapper": @RedisWrapper = - client: => @redis - "settings-sharelatex": @settings - "request": @request - - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } - @ClsiCookieManager = SandboxedModule.require(modulePath, {requires:@requires})() - - - - describe "getServerId", -> - - it "should call get for the key", (done)-> - @redis.get.callsArgWith(1, null, "clsi-7") - @ClsiCookieManager._getServerId @project_id, (err, serverId)=> - @redis.get.calledWith("clsiserver:#{@project_id}").should.equal true - serverId.should.equal "clsi-7" - done() - - it "should _populateServerIdViaRequest if no key is found", (done)-> - @ClsiCookieManager._populateServerIdViaRequest = sinon.stub().callsArgWith(1) - @redis.get.callsArgWith(1, null) - @ClsiCookieManager._getServerId @project_id, (err, serverId)=> - @ClsiCookieManager._populateServerIdViaRequest.calledWith(@project_id).should.equal true - done() - - it "should _populateServerIdViaRequest if no key is blank", (done)-> - @ClsiCookieManager._populateServerIdViaRequest = sinon.stub().callsArgWith(1) - @redis.get.callsArgWith(1, null, "") - @ClsiCookieManager._getServerId @project_id, (err, serverId)=> - @ClsiCookieManager._populateServerIdViaRequest.calledWith(@project_id).should.equal true - done() - - - describe "_populateServerIdViaRequest", -> - - beforeEach -> - @response = "some data" - @request.get.callsArgWith(1, null, @response) - @ClsiCookieManager.setServerId = sinon.stub().callsArgWith(2, null, "clsi-9") - - it "should make a request to the clsi", (done)-> - @ClsiCookieManager._populateServerIdViaRequest @project_id, (err, serverId)=> - args = @ClsiCookieManager.setServerId.args[0] - args[0].should.equal @project_id - args[1].should.deep.equal @response - done() - - it "should return the server id", (done)-> - @ClsiCookieManager._populateServerIdViaRequest @project_id, (err, serverId)=> - serverId.should.equal "clsi-9" - done() - - describe "setServerId", -> - - beforeEach -> - @response = "dsadsakj" - @ClsiCookieManager._parseServerIdFromResponse = sinon.stub().returns("clsi-8") - @redisMulti.exec.callsArgWith(0) - - it "should set the server id with a ttl", (done)-> - @ClsiCookieManager.setServerId @project_id, @response, (err)=> - @redisMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true - @redisMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsiCookie.ttl).should.equal true - done() - - it "should return the server id", (done)-> - @ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=> - serverId.should.equal "clsi-8" - done() - - it "should not set the server id if clsiCookies are not enabled", (done)-> - delete @settings.clsiCookie.key - @ClsiCookieManager = SandboxedModule.require(modulePath, requires:@requires)() - @ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=> - @redisMulti.exec.called.should.equal false - done() - - it "should not set the server id there is no server id in the response", (done)-> - @ClsiCookieManager._parseServerIdFromResponse = sinon.stub().returns(null) - @ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=> - @redisMulti.exec.called.should.equal false - done() - - it "should also set in the secondary if secondary redis is enabled", (done) -> - @redisSecondaryMulti = - set:sinon.stub() - expire:sinon.stub() - exec:sinon.stub() - @redis_secondary = - multi: => @redisSecondaryMulti - @settings.redis.clsi_cookie_secondary = {} - @RedisWrapper.client = sinon.stub() - @RedisWrapper.client.withArgs("clsi_cookie").returns(@redis) - @RedisWrapper.client.withArgs("clsi_cookie_secondary").returns(@redis_secondary) - @ClsiCookieManager = SandboxedModule.require(modulePath, requires:@requires)() - @ClsiCookieManager._parseServerIdFromResponse = sinon.stub().returns("clsi-8") - @ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=> - @redisSecondaryMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true - @redisSecondaryMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsiCookie.ttl).should.equal true - done() - - describe "getCookieJar", -> - - beforeEach -> - @ClsiCookieManager._getServerId = sinon.stub().callsArgWith(1, null, "clsi-11") - - it "should return a jar with the cookie set populated from redis", (done)-> - @ClsiCookieManager.getCookieJar @project_id, (err, jar)=> - jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookie.key].key.should.equal - jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookie.key].value.should.equal "clsi-11" - done() - - - it "should return empty cookie jar if clsiCookies are not enabled", (done)-> - delete @settings.clsiCookie.key - @ClsiCookieManager = SandboxedModule.require(modulePath, requires:@requires)() - @ClsiCookieManager.getCookieJar @project_id, (err, jar)-> - assert.deepEqual jar, realRequst.jar() - done() - - - - - - - diff --git a/services/web/test/unit/coffee/Compile/ClsiFormatCheckerTests.coffee b/services/web/test/unit/coffee/Compile/ClsiFormatCheckerTests.coffee deleted file mode 100644 index e24296060e..0000000000 --- a/services/web/test/unit/coffee/Compile/ClsiFormatCheckerTests.coffee +++ /dev/null @@ -1,151 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Compile/ClsiFormatChecker.js" -SandboxedModule = require('sandboxed-module') - -describe "ClsiFormatChecker", -> - beforeEach -> - @ClsiFormatChecker = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = { compileBodySizeLimitMb: 5 } - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } - @project_id = "project-id" - - - describe "checkRecoursesForProblems", -> - - beforeEach -> - @resources = [{ - path: "main.tex" - content: "stuff" - }, { - path: "chapters/chapter1" - content: "other stuff" - }, { - path: "stuff/image/image.png" - url: "http:somewhere.com/project/#{@project_id}/file/1234124321312" - modified: "more stuff" - }] - - it "should call _checkForDuplicatePaths and _checkForConflictingPaths", (done)-> - - @ClsiFormatChecker._checkForConflictingPaths = sinon.stub().callsArgWith(1, null) - @ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon.stub().callsArgWith(1) - @ClsiFormatChecker.checkRecoursesForProblems @resources, (err, problems)=> - @ClsiFormatChecker._checkForConflictingPaths.called.should.equal true - @ClsiFormatChecker._checkDocsAreUnderSizeLimit.called.should.equal true - done() - - it "should remove undefined errors", (done)-> - @ClsiFormatChecker._checkForConflictingPaths = sinon.stub().callsArgWith(1, null, []) - @ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon.stub().callsArgWith(1, null, {}) - @ClsiFormatChecker.checkRecoursesForProblems @resources, (err, problems)=> - expect(problems).to.not.exist - expect(problems).to.not.exist - done() - - it "should keep populated arrays", (done)-> - @ClsiFormatChecker._checkForConflictingPaths = sinon.stub().callsArgWith(1, null, [{path:"somewhere/main.tex"}]) - @ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon.stub().callsArgWith(1, null, {}) - @ClsiFormatChecker.checkRecoursesForProblems @resources, (err, problems)=> - problems.conflictedPaths[0].path.should.equal "somewhere/main.tex" - expect(problems.sizeCheck).to.not.exist - done() - - it "should keep populated object", (done)-> - @ClsiFormatChecker._checkForConflictingPaths = sinon.stub().callsArgWith(1, null, []) - @ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon.stub().callsArgWith(1, null, {resources:[{"a.tex"},{"b.tex"}], totalSize:1000000}) - @ClsiFormatChecker.checkRecoursesForProblems @resources, (err, problems)=> - problems.sizeCheck.resources.length.should.equal 2 - problems.sizeCheck.totalSize.should.equal 1000000 - expect(problems.conflictedPaths).to.not.exist - done() - - describe "_checkForConflictingPaths", -> - - beforeEach -> - - @resources.push({ - path: "chapters/chapter1.tex" - content: "other stuff" - }) - - @resources.push({ - path: "chapters.tex" - content: "other stuff" - }) - - it "should flag up when a nested file has folder with same subpath as file elsewhere", (done)-> - @resources.push({ - path: "stuff/image" - url: "http://somwhere.com" - }) - - @ClsiFormatChecker._checkForConflictingPaths @resources, (err, conflictPathErrors)-> - conflictPathErrors.length.should.equal 1 - conflictPathErrors[0].path.should.equal "stuff/image" - done() - - it "should flag up when a root level file has folder with same subpath as file elsewhere", (done)-> - @resources.push({ - path: "stuff" - content: "other stuff" - }) - - @ClsiFormatChecker._checkForConflictingPaths @resources, (err, conflictPathErrors)-> - conflictPathErrors.length.should.equal 1 - conflictPathErrors[0].path.should.equal "stuff" - done() - - it "should not flag up when the file is a substring of a path", (done)-> - @resources.push({ - path: "stuf" - content: "other stuff" - }) - - @ClsiFormatChecker._checkForConflictingPaths @resources, (err, conflictPathErrors)-> - conflictPathErrors.length.should.equal 0 - done() - - - describe "_checkDocsAreUnderSizeLimit", -> - - it "should error when there is more than 5mb of data", (done)-> - - @resources.push({ - path: "massive.tex" - content: require("crypto").randomBytes(1000 * 1000 * 5).toString("hex") - }) - - while @resources.length < 20 - @resources.push({path:"chapters/chapter1.tex",url: "http://somwhere.com"}) - - @ClsiFormatChecker._checkDocsAreUnderSizeLimit @resources, (err, sizeError)-> - sizeError.totalSize.should.equal 10000016 - sizeError.resources.length.should.equal 10 - sizeError.resources[0].path.should.equal "massive.tex" - sizeError.resources[0].size.should.equal 1000 * 1000 * 10 - done() - - - it "should return nothing when project is correct size", (done)-> - - @resources.push({ - path: "massive.tex" - content: require("crypto").randomBytes(1000 * 1000 * 1).toString("hex") - }) - - while @resources.length < 20 - @resources.push({path:"chapters/chapter1.tex",url: "http://somwhere.com"}) - - @ClsiFormatChecker._checkDocsAreUnderSizeLimit @resources, (err, sizeError)-> - expect(sizeError).to.not.exist - done() - - - - - - - diff --git a/services/web/test/unit/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/unit/coffee/Compile/ClsiManagerTests.coffee deleted file mode 100644 index e7676337e4..0000000000 --- a/services/web/test/unit/coffee/Compile/ClsiManagerTests.coffee +++ /dev/null @@ -1,585 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Compile/ClsiManager.js" -SandboxedModule = require('sandboxed-module') - -describe "ClsiManager", -> - beforeEach -> - @jar = {cookie:"stuff"} - @ClsiCookieManager = - getCookieJar: sinon.stub().callsArgWith(1, null, @jar) - setServerId: sinon.stub().callsArgWith(2) - _getServerId:sinon.stub() - @ClsiStateManager = - computeHash: sinon.stub().callsArgWith(2, null, "01234567890abcdef") - @ClsiFormatChecker = - checkRecoursesForProblems:sinon.stub().callsArgWith(1) - @ClsiManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = - apis: - filestore: - url: "filestore.example.com" - secret: "secret" - clsi: - url: "http://clsi.example.com" - clsi_priority: - url: "https://clsipremium.example.com" - "../../models/Project": Project: @Project = {} - "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} - "../Project/ProjectGetter": @ProjectGetter = {} - "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = - getProjectDocsIfMatch: sinon.stub().callsArgWith(2,null,null) - "./ClsiCookieManager": => @ClsiCookieManager - "./ClsiStateManager": @ClsiStateManager - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err: sinon.stub(), warn: sinon.stub() } - "request": @request = sinon.stub() - "./ClsiFormatChecker": @ClsiFormatChecker - "metrics-sharelatex": @Metrics = - Timer: class Timer - done: sinon.stub() - inc: sinon.stub() - @project_id = "project-id" - @user_id = "user-id" - @callback = sinon.stub() - - describe "sendRequest", -> - beforeEach -> - @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @request = "mock-request") - @ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3") - - describe "with a successful compile", -> - beforeEach -> - @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { - compile: - status: @status = "success" - outputFiles: [{ - url: "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.pdf" - path: "output.pdf" - type: "pdf" - build: 1234 - },{ - url: "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.log" - path: "output.log" - type: "log" - build: 1234 - }] - }) - @ClsiManager.sendRequest @project_id, @user_id, {compileGroup:"standard"}, @callback - - it "should build the request", -> - @ClsiManager._buildRequest - .calledWith(@project_id) - .should.equal true - - it "should send the request to the CLSI", -> - @ClsiManager._postToClsi - .calledWith(@project_id, @user_id, @request, "standard") - .should.equal true - - it "should call the callback with the status and output files", -> - outputFiles = [{ - url: "/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.pdf" - path: "output.pdf" - type: "pdf" - build: 1234 - },{ - url: "/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.log" - path: "output.log" - type: "log" - build: 1234 - }] - @callback.calledWith(null, @status, outputFiles).should.equal true - - describe "with a failed compile", -> - beforeEach -> - @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { - compile: - status: @status = "failure" - }) - @ClsiManager.sendRequest @project_id, @user_id, {}, @callback - - it "should call the callback with a failure status", -> - @callback.calledWith(null, @status).should.equal true - - describe "with a sync conflict", -> - beforeEach -> - @ClsiManager.sendRequestOnce = sinon.stub() - @ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {syncType:"full"}).callsArgWith(3, null, @status = "success") - @ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {}).callsArgWith(3, null, "conflict") - @ClsiManager.sendRequest @project_id, @user_id, {}, @callback - - it "should call the sendRequestOnce method twice", -> - @ClsiManager.sendRequestOnce.calledTwice.should.equal true - - it "should call the sendRequestOnce method with syncType:full", -> - @ClsiManager.sendRequestOnce.calledWith(@project_id, @user_id, {syncType:"full"}).should.equal true - - it "should call the sendRequestOnce method without syncType:full", -> - @ClsiManager.sendRequestOnce.calledWith(@project_id, @user_id, {}).should.equal true - - it "should call the callback with a success status", -> - @callback.calledWith(null, @status, ).should.equal true - - describe "when the resources fail the precompile check", -> - beforeEach -> - @ClsiFormatChecker.checkRecoursesForProblems = sinon.stub().callsArgWith(1, new Error("failed")) - @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { - compile: - status: @status = "failure" - }) - @ClsiManager.sendRequest @project_id, @user_id, {}, @callback - - it "should call the callback only once", -> - @callback.calledOnce.should.equal true - - it "should call the callback with an error", -> - @callback.calledWithExactly(new Error("failed")).should.equal true - - describe "sendExternalRequest", -> - beforeEach -> - @submission_id = "submission-id" - @clsi_request = "mock-request" - @ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3") - - describe "with a successful compile", -> - beforeEach -> - @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { - compile: - status: @status = "success" - outputFiles: [{ - url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.pdf" - path: "output.pdf" - type: "pdf" - build: 1234 - },{ - url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.log" - path: "output.log" - type: "log" - build: 1234 - }] - }) - @ClsiManager.sendExternalRequest @submission_id, @clsi_request, {compileGroup:"standard"}, @callback - - it "should send the request to the CLSI", -> - @ClsiManager._postToClsi - .calledWith(@submission_id, null, @clsi_request, "standard") - .should.equal true - - it "should call the callback with the status and output files", -> - outputFiles = [{ - url: "/project/#{@submission_id}/build/1234/output/output.pdf" - path: "output.pdf" - type: "pdf" - build: 1234 - },{ - url: "/project/#{@submission_id}/build/1234/output/output.log" - path: "output.log" - type: "log" - build: 1234 - }] - @callback.calledWith(null, @status, outputFiles).should.equal true - - describe "with a failed compile", -> - beforeEach -> - @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { - compile: - status: @status = "failure" - }) - @ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback - - it "should call the callback with a failure status", -> - @callback.calledWith(null, @status).should.equal true - - describe "when the resources fail the precompile check", -> - beforeEach -> - @ClsiFormatChecker.checkRecoursesForProblems = sinon.stub().callsArgWith(1, new Error("failed")) - @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { - compile: - status: @status = "failure" - }) - @ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback - - it "should call the callback only once", -> - @callback.calledOnce.should.equal true - - it "should call the callback with an error", -> - @callback.calledWithExactly(new Error("failed")).should.equal true - - - describe "deleteAuxFiles", -> - beforeEach -> - @ClsiManager._makeRequest = sinon.stub().callsArg(2) - @DocumentUpdaterHandler.clearProjectState = sinon.stub().callsArg(1) - - describe "with the standard compileGroup", -> - beforeEach -> - @ClsiManager.deleteAuxFiles @project_id, @user_id, {compileGroup: "standard"}, @callback - - it "should call the delete method in the standard CLSI", -> - @ClsiManager._makeRequest - .calledWith(@project_id, { method:"DELETE", url:"#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}"}) - .should.equal true - - it "should clear the project state from the docupdater", -> - @DocumentUpdaterHandler.clearProjectState - .calledWith(@project_id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - - describe "_buildRequest", -> - beforeEach -> - @project = - _id: @project_id - compiler: @compiler = "latex" - rootDoc_id: "mock-doc-id-1" - imageName: @image = "mock-image-name" - - @docs = { - "/main.tex": @doc_1 = { - name: "main.tex" - _id: "mock-doc-id-1" - lines: ["Hello", "world"] - }, - "/chapters/chapter1.tex": @doc_2 = { - name: "chapter1.tex" - _id: "mock-doc-id-2" - lines: [ - "Chapter 1" - ] - } - } - - @files = { - "/images/image.png": @file_1 = { - name: "image.png" - _id: "mock-file-id-1" - created: new Date() - } - } - - @Project.findById = sinon.stub().callsArgWith(2, null, @project) - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files) - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null) - - describe "with a valid project", -> - beforeEach (done) -> - @ClsiManager._buildRequest @project_id, {timeout:100}, (error, request) => - @request = request - done() - - it "should get the project with the required fields", -> - @ProjectGetter.getProject - .calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1, rootFolder: 1}) - .should.equal true - - it "should flush the project to the database", -> - @DocumentUpdaterHandler.flushProjectToMongo - .calledWith(@project_id) - .should.equal true - - it "should get all the docs", -> - @ProjectEntityHandler.getAllDocs - .calledWith(@project_id) - .should.equal true - - it "should get all the files", -> - @ProjectEntityHandler.getAllFiles - .calledWith(@project_id) - .should.equal true - - it "should build up the CLSI request", -> - expect(@request).to.deep.equal( - compile: - options: - compiler: @compiler - timeout : 100 - imageName: @image - draft: false - check: undefined - syncType: undefined # "full" - syncState: undefined # "01234567890abcdef" - rootResourcePath: "main.tex" - resources: [{ - path: "main.tex" - content: @doc_1.lines.join("\n") - }, { - path: "chapters/chapter1.tex" - content: @doc_2.lines.join("\n") - }, { - path: "images/image.png" - url: "#{@settings.apis.filestore.url}/project/#{@project_id}/file/#{@file_1._id}" - modified: @file_1.created.getTime() - }] - ) - - describe "with the incremental compile option", -> - beforeEach (done) -> - @ClsiStateManager.computeHash = sinon.stub().callsArgWith(2, null, @project_state_hash = "01234567890abcdef") - @DocumentUpdaterHandler.getProjectDocsIfMatch = sinon.stub().callsArgWith(2, null, [{_id:@doc_1._id, lines: @doc_1.lines, v: 123}]) - @ProjectEntityHandler.getAllDocPathsFromProject = sinon.stub().callsArgWith(1, null, {"mock-doc-id-1":"main.tex"}) - @ClsiManager._buildRequest @project_id, {timeout:100, incrementalCompilesEnabled:true}, (error, request) => - @request = request - done() - - it "should get the project with the required fields", -> - @ProjectGetter.getProject - .calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1, rootFolder: 1}) - .should.equal true - - it "should not explicitly flush the project to the database", -> - @DocumentUpdaterHandler.flushProjectToMongo - .calledWith(@project_id) - .should.equal false - - it "should get only the live docs from the docupdater with a background flush in docupdater", -> - @DocumentUpdaterHandler.getProjectDocsIfMatch - .calledWith(@project_id) - .should.equal true - - it "should not get any of the files", -> - @ProjectEntityHandler.getAllFiles - .called.should.equal false - - it "should build up the CLSI request", -> - expect(@request).to.deep.equal( - compile: - options: - compiler: @compiler - timeout : 100 - imageName: @image - draft: false - check: undefined - syncType: "incremental" - syncState: "01234567890abcdef" - rootResourcePath: "main.tex" - resources: [{ - path: "main.tex" - content: @doc_1.lines.join("\n") - }] - ) - - - describe "when the root doc is set and not in the docupdater", -> - beforeEach (done) -> - @ClsiStateManager.computeHash = sinon.stub().callsArgWith(2, null, @project_state_hash = "01234567890abcdef") - @DocumentUpdaterHandler.getProjectDocsIfMatch = sinon.stub().callsArgWith(2, null, [{_id:@doc_1._id, lines: @doc_1.lines, v: 123}]) - @ProjectEntityHandler.getAllDocPathsFromProject = sinon.stub().callsArgWith(1, null, {"mock-doc-id-1":"main.tex", "mock-doc-id-2":"/chapters/chapter1.tex"}) - @ClsiManager._buildRequest @project_id, {timeout:100, incrementalCompilesEnabled:true, rootDoc_id:"mock-doc-id-2"}, (error, request) => - @request = request - done() - - it "should still change the root path", -> - @request.compile.rootResourcePath.should.equal "chapters/chapter1.tex" - - describe "when root doc override is valid", -> - beforeEach (done) -> - @ClsiManager._buildRequest @project_id, {rootDoc_id:"mock-doc-id-2"}, (error, request) => - @request = request - done() - - it "should change root path", -> - @request.compile.rootResourcePath.should.equal "chapters/chapter1.tex" - - - describe "when root doc override is invalid", -> - beforeEach (done) -> - @ClsiManager._buildRequest @project_id, {rootDoc_id:"invalid-id"}, (error, request) => - @request = request - done() - - it "should fallback to default root doc", -> - @request.compile.rootResourcePath.should.equal "main.tex" - - - - describe "when the project has an invalid compiler", -> - beforeEach (done) -> - @project.compiler = "context" - @ClsiManager._buildRequest @project, null, (error, request) => - @request = request - done() - - it "should set the compiler to pdflatex", -> - @request.compile.options.compiler.should.equal "pdflatex" - - describe "when there is no valid root document", -> - beforeEach (done) -> - @project.rootDoc_id = "not-valid" - @ClsiManager._buildRequest @project, null, (@error, @request) => - done() - - it "should set to main.tex", -> - @request.compile.rootResourcePath.should.equal "main.tex" - - describe "when there is no valid root document and no main.tex document", -> - beforeEach () -> - @project.rootDoc_id = "not-valid" - @docs = { - "/other.tex": @doc_1 = { - name: "other.tex" - _id: "mock-doc-id-1" - lines: ["Hello", "world"] - }, - "/chapters/chapter1.tex": @doc_2 = { - name: "chapter1.tex" - _id: "mock-doc-id-2" - lines: [ - "Chapter 1" - ] - } - } - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ClsiManager._buildRequest @project, null, @callback - - it "should report an error", -> - @callback.calledWith(new Error("no main file specified")).should.equal true - - - describe "when there is no valid root document and a single document which is not main.tex", -> - beforeEach (done) -> - @project.rootDoc_id = "not-valid" - @docs = { - "/other.tex": @doc_1 = { - name: "other.tex" - _id: "mock-doc-id-1" - lines: ["Hello", "world"] - } - } - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ClsiManager._buildRequest @project, null, (@error, @request) => - done() - - it "should set io to the only file", -> - @request.compile.rootResourcePath.should.equal "other.tex" - - - describe "with the draft option", -> - it "should add the draft option into the request", (done) -> - @ClsiManager._buildRequest @project_id, {timeout:100, draft: true}, (error, request) => - request.compile.options.draft.should.equal true - done() - - - describe '_postToClsi', -> - beforeEach -> - @req = { mock: "req" } - - describe "successfully", -> - beforeEach -> - @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 204}, @body = { mock: "foo" }) - @ClsiManager._postToClsi @project_id, @user_id, @req, "standard", @callback - - it 'should send the request to the CLSI', -> - url = "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/compile" - @ClsiManager._makeRequest.calledWith(@project_id, { - method: "POST", - url: url - json: @req - }).should.equal true - - it "should call the callback with the body and no error", -> - @callback.calledWith(null, @body).should.equal true - - describe "when the CLSI returns an error", -> - beforeEach -> - @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 500}, @body = { mock: "foo" }) - @ClsiManager._postToClsi @project_id, @user_id, @req, "standard", @callback - - it "should call the callback with the body and the error", -> - @callback.calledWith(new Error("CLSI returned non-success code: 500"), @body).should.equal true - - - describe "wordCount", -> - beforeEach -> - @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 200}, @body = { mock: "foo" }) - @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @req = { compile: { rootResourcePath: "rootfile.text", options: {} } }) - - describe "with root file", -> - beforeEach -> - @ClsiManager.wordCount @project_id, @user_id, false, {}, @callback - - it "should call wordCount with root file", -> - @ClsiManager._makeRequest - .calledWith(@project_id, {method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs: {file: "rootfile.text",image:undefined}}) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "with param file", -> - beforeEach -> - @ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback - - it "should call wordCount with param file", -> - @ClsiManager._makeRequest - .calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:undefined}}) - .should.equal true - - describe "with image", -> - beforeEach -> - @req.compile.options.imageName = @image = "example.com/mock/image" - @ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback - - it "should call wordCount with file and image", -> - @ClsiManager._makeRequest - .calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:@image}}) - .should.equal true - - - - describe "_makeRequest", -> - - beforeEach -> - @response = {there:"something"} - @request.callsArgWith(1, null, @response) - @opts = - method: "SOMETHIGN" - url: "http://a place on the web" - - it "should process a request with a cookie jar", (done)-> - @ClsiManager._makeRequest @project_id, @opts, => - args = @request.args[0] - args[0].method.should.equal @opts.method - args[0].url.should.equal @opts.url - args[0].jar.should.equal @jar - done() - - it "should set the cookie again on response as it might have changed", (done)-> - @ClsiManager._makeRequest @project_id, @opts, => - @ClsiCookieManager.setServerId.calledWith(@project_id, @response).should.equal true - done() - - - describe "_makeGoogleCloudRequest", -> - - beforeEach -> - @settings.apis.clsi_new = - url : "https://compiles.somewhere.test" - @response = {there:"something"} - @request.callsArgWith(1, null, @response) - @opts = - url: @ClsiManager._getCompilerUrl(null, @project_id) - - it "should change the domain on the url", (done)-> - @ClsiManager._makeNewBackendRequest @project_id, @opts, => - args = @request.args[0] - args[0].url.should.equal "https://compiles.somewhere.test/project/#{@project_id}" - done() - - it "should not make a request if there is not clsi_new url", (done)-> - @settings.apis.clsi_new = undefined - @ClsiManager._makeNewBackendRequest @project_id, @opts, (err)=> - expect(err).to.equal undefined - @request.callCount.should.equal 0 - done() - - - - diff --git a/services/web/test/unit/coffee/Compile/ClsiStateManagerTests.coffee b/services/web/test/unit/coffee/Compile/ClsiStateManagerTests.coffee deleted file mode 100644 index 3629ca46cf..0000000000 --- a/services/web/test/unit/coffee/Compile/ClsiStateManagerTests.coffee +++ /dev/null @@ -1,171 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Compile/ClsiStateManager.js" -SandboxedModule = require('sandboxed-module') - -describe "ClsiStateManager", -> - beforeEach -> - @ClsiStateManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = {} - "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } - @project = "project" - @options = {"draft":true,"isAutoCompile":false} - @callback = sinon.stub() - - describe "computeHash", -> - beforeEach (done) -> - @docs = [ - {path: "/main.tex", doc: {_id: "doc-id-1"}} - {path: "/folder/sub.tex", doc: {_id: "doc-id-2"}} - ] - @files = [ - {path: "/figure.pdf", file: {_id: "file-id-1", rev: 123, created: "aaaaaa"}} - {path: "/folder/fig2.pdf", file: {_id: "file-id-2", rev: 456, created: "bbbbbb"}} - ] - @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files) - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash0 = hash - done() - - describe "with a sample project", -> - beforeEach -> - @ClsiStateManager.computeHash @project, @options, @callback - - it "should call the callback with a hash value", -> - @callback - .calledWith(null, "21b1ab73aa3892bec452baf8ffa0956179e1880f") - .should.equal true - - describe "when the files and docs are in a different order", -> - beforeEach -> - [@docs[0], @docs[1]] = [@docs[1], @docs[0]] - [@files[0], @files[1]] = [@files[1], @files[0]] - @ClsiStateManager.computeHash @project, @options, @callback - - it "should call the callback with the same hash value", -> - @callback - .calledWith(null, @hash0) - .should.equal true - - describe "when a doc is renamed", -> - beforeEach (done) -> - @docs[0].path = "/new.tex" - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - describe "when a file is renamed", -> - beforeEach (done) -> - @files[0].path = "/newfigure.pdf" - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - describe "when a doc is added", -> - beforeEach (done) -> - @docs.push { path: "/newdoc.tex", doc: {_id: "newdoc-id"}} - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - describe "when a file is added", -> - beforeEach (done) -> - @files.push { path: "/newfile.tex", file: {_id: "newfile-id", rev: 123}} - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - describe "when a doc is removed", -> - beforeEach (done) -> - @docs.pop() - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - describe "when a file is removed", -> - beforeEach (done) -> - @files.pop() - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - describe "when a file's revision is updated", -> - beforeEach (done) -> - @files[0].file.rev++ - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - - describe "when a file's date is updated", -> - beforeEach (done) -> - @files[0].file.created = "zzzzzz" - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - describe "when the compile options are changed", -> - beforeEach (done) -> - @options.draft = !@options.draft - @ClsiStateManager.computeHash @project, @options, (err, hash) => - @hash1 = hash - done() - - it "should call the callback with a different hash value", -> - @callback - .neverCalledWith(null, @hash0) - .should.equal true - - - describe "when the isAutoCompile option is changed", -> - beforeEach () -> - @options.isAutoCompile = !@options.isAutoCompile - @ClsiStateManager.computeHash @project, @options, @callback - - it "should call the callback with the same hash value", -> - @callback - .calledWith(null, @hash0) - .should.equal true diff --git a/services/web/test/unit/coffee/Compile/CompileControllerTests.coffee b/services/web/test/unit/coffee/Compile/CompileControllerTests.coffee deleted file mode 100644 index fa8fb7ea92..0000000000 --- a/services/web/test/unit/coffee/Compile/CompileControllerTests.coffee +++ /dev/null @@ -1,486 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -assert = require("chai").assert -expect = chai.expect -modulePath = "../../../../app/js/Features/Compile/CompileController.js" -SandboxedModule = require('sandboxed-module') -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" - -describe "CompileController", -> - beforeEach -> - @user_id = 'wat' - @user = - _id: @user_id - email: 'user@example.com' - features: - compileGroup: "premium" - compileTimeout: 100 - @CompileManager = - compile: sinon.stub() - @ClsiManager = {} - @UserGetter = - getUser:sinon.stub() - @RateLimiter = {addCount:sinon.stub()} - @settings = - apis: - clsi: - url: "clsi.example.com" - clsi_priority: - url: "clsi-priority.example.com" - defaultFeatures: - compileGroup: 'standard' - compileTimeout: 60 - @jar = {cookie:"stuff"} - @ClsiCookieManager = - getCookieJar:sinon.stub().callsArgWith(1, null, @jar) - @AuthenticationController = - getLoggedInUser: sinon.stub().callsArgWith(1, null, @user) - getLoggedInUserId: sinon.stub().returns(@user_id) - getSessionUser: sinon.stub().returns(@user) - isUserLoggedIn: sinon.stub().returns(true) - @CompileController = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "request": @request = sinon.stub() - '../Project/ProjectGetter': @ProjectGetter = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } - "metrics-sharelatex": @Metrics = { inc: sinon.stub() } - "./CompileManager":@CompileManager - "../User/UserGetter":@UserGetter - "./ClsiManager": @ClsiManager - "../Authentication/AuthenticationController": @AuthenticationController - "../../infrastructure/RateLimiter":@RateLimiter - "./ClsiCookieManager": ()=> @ClsiCookieManager - @project_id = "project-id" - @next = sinon.stub() - @req = new MockRequest() - @res = new MockResponse() - - describe "compile", -> - beforeEach -> - @req.params = - Project_id: @project_id - @req.session = {} - @CompileManager.compile = sinon.stub().callsArgWith(3, null, @status = "success", @outputFiles = ["mock-output-files"]) - - describe "when not an auto compile", -> - beforeEach -> - @CompileController.compile @req, @res, @next - - it "should look up the user id", -> - @AuthenticationController.getLoggedInUserId - .calledWith(@req) - .should.equal true - - it "should do the compile without the auto compile flag", -> - @CompileManager.compile - .calledWith(@project_id, @user_id, { isAutoCompile: false }) - .should.equal true - - it "should set the content-type of the response to application/json", -> - @res.contentType - .calledWith("application/json") - .should.equal true - - it "should send a successful response reporting the status and files", -> - @res.statusCode.should.equal 200 - @res.body.should.equal JSON.stringify({ - status: @status - outputFiles: @outputFiles - }) - - describe "when an auto compile", -> - beforeEach -> - @req.query = - auto_compile: "true" - @CompileController.compile @req, @res, @next - - it "should do the compile with the auto compile flag", -> - @CompileManager.compile - .calledWith(@project_id, @user_id, { isAutoCompile: true }) - .should.equal true - - describe "with the draft attribute", -> - beforeEach -> - @req.body = - draft: true - @CompileController.compile @req, @res, @next - - it "should do the compile without the draft compile flag", -> - @CompileManager.compile - .calledWith(@project_id, @user_id, { isAutoCompile: false, draft: true }) - .should.equal true - - describe "compileSubmission", -> - beforeEach -> - @submission_id = 'sub-1234' - @req.params = - submission_id: @submission_id - @req.body = {} - @ClsiManager.sendExternalRequest = sinon.stub() - .callsArgWith(3, null, @status = "success", @outputFiles = ["mock-output-files"], \ - @clsiServerId = "mock-server-id", @validationProblems = null) - - it "should set the content-type of the response to application/json", -> - @CompileController.compileSubmission @req, @res, @next - @res.contentType - .calledWith("application/json") - .should.equal true - - it "should send a successful response reporting the status and files", -> - @CompileController.compileSubmission @req, @res, @next - @res.statusCode.should.equal 200 - @res.body.should.equal JSON.stringify({ - status: @status - outputFiles: @outputFiles - clsiServerId: 'mock-server-id' - validationProblems: null - }) - - describe "with compileGroup and timeout", -> - beforeEach -> - @req.body = - compileGroup: 'special' - timeout: 600 - @CompileController.compileSubmission @req, @res, @next - - it "should use the supplied values", -> - @ClsiManager.sendExternalRequest - .calledWith(@submission_id, { compileGroup: 'special', timeout: 600 }, \ - { compileGroup: 'special', timeout: 600 }) - .should.equal true - - describe "with other supported options but not compileGroup and timeout", -> - beforeEach -> - @req.body = - rootResourcePath: 'main.tex' - compiler: 'lualatex' - draft: true - check: 'validate' - @CompileController.compileSubmission @req, @res, @next - - it "should use the other options but default values for compileGroup and timeout", -> - @ClsiManager.sendExternalRequest - .calledWith(@submission_id, \ - {rootResourcePath: 'main.tex', compiler: 'lualatex', draft: true, check: 'validate'}, \ - {rootResourcePath: 'main.tex', compiler: 'lualatex', draft: true, check: 'validate', \ - compileGroup: 'standard', timeout: 60}) - .should.equal true - - describe "downloadPdf", -> - beforeEach -> - @req.params = - Project_id: @project_id - - @req.query = {pdfng:true} - @project = name: "test namè" - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - - describe "when downloading for embedding", -> - beforeEach -> - @CompileController.proxyToClsi = sinon.stub() - @RateLimiter.addCount.callsArgWith(1, null, true) - @CompileController.downloadPdf(@req, @res, @next) - - it "should look up the project", -> - @ProjectGetter.getProject - .calledWith(@project_id, {name: 1}) - .should.equal true - - it "should set the content-type of the response to application/pdf", -> - @res.contentType - .calledWith("application/pdf") - .should.equal true - - it "should set the content-disposition header with a safe version of the project name", -> - @res.setContentDisposition - .calledWith('', filename: "test_nam_.pdf") - .should.equal true - - it "should increment the pdf-downloads metric", -> - @Metrics.inc - .calledWith("pdf-downloads") - .should.equal true - - it "should proxy the PDF from the CLSI", -> - @CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/user/#{@user_id}/output/output.pdf", @req, @res, @next).should.equal true - - describe "when the a build-id is provided", -> - beforeEach -> - @req.params.build_id = @buildId = '1234-5678' - @CompileController.proxyToClsi = sinon.stub() - @RateLimiter.addCount.callsArgWith(1, null, true) - @CompileController.downloadPdf(@req, @res, @next) - - it "should proxy the PDF from the CLSI, with a build-id", -> - @CompileController.proxyToClsi.calledWith( - @project_id, - "/project/#{@project_id}/user/#{@user_id}/build/#{@buildId}/output/output.pdf", - @req, @res, @next - ).should.equal true - - describe "when the pdf is not going to be used in pdfjs viewer", -> - - it "should check the rate limiter when pdfng is not set", (done)-> - @req.query = {} - @RateLimiter.addCount.callsArgWith(1, null, true) - @CompileController.proxyToClsi = (project_id, url)=> - @RateLimiter.addCount.args[0][0].throttle.should.equal 1000 - done() - @CompileController.downloadPdf @req, @res - - - it "should check the rate limiter when pdfng is false", (done)-> - @req.query = {pdfng:false} - @RateLimiter.addCount.callsArgWith(1, null, true) - @CompileController.proxyToClsi = (project_id, url)=> - @RateLimiter.addCount.args[0][0].throttle.should.equal 1000 - done() - @CompileController.downloadPdf @req, @res - - describe "getFileFromClsiWithoutUser", -> - beforeEach -> - @submission_id = 'sub-1234' - @build_id = 123456 - @file = 'project.pdf' - @req.params = - submission_id: @submission_id - build_id: @build_id - file: @file - @req.body = {} - @expected_url = "/project/#{@submission_id}/build/#{@build_id}/output/#{@file}" - @CompileController.proxyToClsiWithLimits = sinon.stub() - - describe "without limits specified", -> - beforeEach -> - @CompileController.getFileFromClsiWithoutUser @req, @res, @next - - it "should proxy to CLSI with correct URL and default limits", -> - @CompileController.proxyToClsiWithLimits - .calledWith(@submission_id, @expected_url, {compileGroup: 'standard'}) - .should.equal true - - describe "with limits specified", -> - beforeEach -> - @req.body = {compileTimeout: 600, compileGroup: 'special'} - @CompileController.getFileFromClsiWithoutUser @req, @res, @next - - it "should proxy to CLSI with correct URL and specified limits", -> - @CompileController.proxyToClsiWithLimits - .calledWith(@submission_id, @expected_url, {compileGroup: 'special'}) - .should.equal true - - describe "proxyToClsi", -> - beforeEach -> - @request.returns(@proxy = { - pipe: sinon.stub() - on: sinon.stub() - }) - @upstream = - statusCode: 204 - headers: { "mock": "header" } - @req.method = "mock-method" - @req.headers = { - 'Mock': 'Headers', - 'Range': '123-456' - 'If-Range': 'abcdef' - 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT' - } - - describe "old pdf viewer", -> - describe "user with standard priority", -> - beforeEach -> - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, {compileGroup: "standard"}) - @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) - - it "should open a request to the CLSI", -> - @request - .calledWith( - jar:@jar - method: @req.method - url: "#{@settings.apis.clsi.url}#{@url}", - timeout: 60 * 1000 - ) - .should.equal true - - it "should pass the request on to the client", -> - @proxy.pipe - .calledWith(@res) - .should.equal true - - it "should bind an error handle to the request proxy", -> - @proxy.on.calledWith("error").should.equal true - - describe "user with priority compile", -> - beforeEach -> - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, {compileGroup: "priority"}) - @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) - - describe "user with standard priority via query string", -> - beforeEach -> - @req.query = {compileGroup: 'standard'} - @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) - - it "should open a request to the CLSI", -> - @request - .calledWith( - jar:@jar - method: @req.method - url: "#{@settings.apis.clsi.url}#{@url}", - timeout: 60 * 1000 - ) - .should.equal true - - it "should pass the request on to the client", -> - @proxy.pipe - .calledWith(@res) - .should.equal true - - it "should bind an error handle to the request proxy", -> - @proxy.on.calledWith("error").should.equal true - - - describe "user with non-existent priority via query string", -> - beforeEach -> - @req.query = {compileGroup: 'foobar'} - @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) - - it "should proxy to the standard url", ()-> - @request - .calledWith( - jar:@jar - method: @req.method - url: "#{@settings.apis.clsi.url}#{@url}", - timeout: 60 * 1000 - ) - .should.equal true - - describe "user with build parameter via query string", -> - beforeEach -> - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, {compileGroup: "standard"}) - @req.query = {build: 1234} - @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) - - it "should proxy to the standard url without the build parameter", ()-> - @request - .calledWith( - jar:@jar - method: @req.method - url: "#{@settings.apis.clsi.url}#{@url}", - timeout: 60 * 1000 - ) - .should.equal true - - describe "new pdf viewer", -> - beforeEach -> - @req.query = {pdfng: true} - describe "user with standard priority", -> - beforeEach -> - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, {compileGroup: "standard"}) - @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) - - it "should open a request to the CLSI", -> - @request - .calledWith( - jar:@jar - method: @req.method - url: "#{@settings.apis.clsi.url}#{@url}", - timeout: 60 * 1000 - headers: { - 'Range': '123-456' - 'If-Range': 'abcdef' - 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT' - } - ) - .should.equal true - - it "should pass the request on to the client", -> - @proxy.pipe - .calledWith(@res) - .should.equal true - - it "should bind an error handle to the request proxy", -> - @proxy.on.calledWith("error").should.equal true - - - - describe "user with build parameter via query string", -> - beforeEach -> - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, {compileGroup: "standard"}) - @req.query = {build: 1234, pdfng: true} - @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) - - it "should proxy to the standard url with the build parameter", ()-> - @request.calledWith( - jar:@jar - method: @req.method - qs: {build: 1234} - url: "#{@settings.apis.clsi.url}#{@url}", - timeout: 60 * 1000 - headers: { - 'Range': '123-456' - 'If-Range': 'abcdef' - 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT' - } - ) - .should.equal true - - describe "deleteAuxFiles", -> - beforeEach -> - @CompileManager.deleteAuxFiles = sinon.stub().callsArg(2) - @req.params = - Project_id: @project_id - @res.sendStatus = sinon.stub() - @CompileController.deleteAuxFiles @req, @res, @next - - it "should proxy to the CLSI", -> - @CompileManager.deleteAuxFiles - .calledWith(@project_id) - .should.equal true - - it "should return a 200", -> - @res.sendStatus - .calledWith(200) - .should.equal true - - describe "compileAndDownloadPdf", -> - beforeEach -> - @req = - params: - project_id:@project_id - @CompileManager.compile.callsArgWith(3) - @CompileController.proxyToClsi = sinon.stub() - @res = - send:=> - - it "should call compile in the compile manager", (done)-> - @CompileController.compileAndDownloadPdf @req, @res - @CompileManager.compile.calledWith(@project_id).should.equal true - done() - - it "should proxy the res to the clsi with correct url", (done)-> - @CompileController.compileAndDownloadPdf @req, @res - sinon.assert.calledWith @CompileController.proxyToClsi, @project_id, "/project/#{@project_id}/output/output.pdf", @req, @res - - @CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/output/output.pdf", @req, @res).should.equal true - done() - - describe "wordCount", -> - beforeEach -> - @CompileManager.wordCount = sinon.stub().callsArgWith(3, null, {content:"body"}) - @req.params = - Project_id: @project_id - @res.send = sinon.stub() - @res.contentType = sinon.stub() - @CompileController.wordCount @req, @res, @next - - it "should proxy to the CLSI", -> - @CompileManager.wordCount - .calledWith(@project_id, @user_id, false) - .should.equal true - - it "should return a 200 and body", -> - @res.send - .calledWith({content:"body"}) - .should.equal true diff --git a/services/web/test/unit/coffee/Compile/CompileManagerTests.coffee b/services/web/test/unit/coffee/Compile/CompileManagerTests.coffee deleted file mode 100644 index 890a02ac24..0000000000 --- a/services/web/test/unit/coffee/Compile/CompileManagerTests.coffee +++ /dev/null @@ -1,222 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Compile/CompileManager.js" -assert = require("chai").assert -SandboxedModule = require('sandboxed-module') - -describe "CompileManager", -> - beforeEach -> - @rateLimitGetStub = sinon.stub() - rateLimitGetStub = @rateLimitGetStub - @ratelimiter = - addCount: sinon.stub() - @CompileManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = - redis: web: {host: "localhost", port: 42} - "../../infrastructure/RedisWrapper": - client: () => @rclient = { auth: () -> } - "../Project/ProjectRootDocManager": @ProjectRootDocManager = {} - '../Project/ProjectGetter': @ProjectGetter = {} - "../User/UserGetter": @UserGetter = {} - "./ClsiManager": @ClsiManager = {} - "../../infrastructure/RateLimiter": @ratelimiter - "metrics-sharelatex": @Metrics = - Timer: class Timer - done: sinon.stub() - inc: sinon.stub() - "logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub() } - @project_id = "mock-project-id-123" - @user_id = "mock-user-id-123" - @callback = sinon.stub() - @limits = { - timeout: 42 - } - - - describe "compile", -> - beforeEach -> - @CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false) - @ProjectRootDocManager.ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null) - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith(1, null, @limits) - @ClsiManager.sendRequest = sinon.stub().callsArgWith(3, null, @status = "mock-status", @outputFiles = "mock output files", @output = "mock output") - - describe "succesfully", -> - beforeEach -> - @CompileManager._checkIfAutoCompileLimitHasBeenHit = (isAutoCompile, compileGroup, cb)-> cb(null, true) - @CompileManager.compile @project_id, @user_id, {}, @callback - - it "should check the project has not been recently compiled", -> - @CompileManager._checkIfRecentlyCompiled - .calledWith(@project_id, @user_id) - .should.equal true - - it "should ensure that the root document is set", -> - @ProjectRootDocManager.ensureRootDocumentIsSet - .calledWith(@project_id) - .should.equal true - - it "should get the project compile limits", -> - @CompileManager.getProjectCompileLimits - .calledWith(@project_id) - .should.equal true - - it "should run the compile with the compile limits", -> - @ClsiManager.sendRequest - .calledWith(@project_id, @user_id, { - timeout: @limits.timeout - }) - .should.equal true - - it "should call the callback with the output", -> - @callback - .calledWith(null, @status, @outputFiles, @output) - .should.equal true - - it "should time the compile", -> - @Metrics.Timer::done.called.should.equal true - - it "should log out the compile", -> - @logger.log - .calledWith(project_id: @project_id, user_id: @user_id, "compiling project") - .should.equal true - - describe "when the project has been recently compiled", -> - it "should return", (done)-> - @CompileManager._checkIfAutoCompileLimitHasBeenHit = (isAutoCompile, compileGroup, cb)-> cb(null, true) - @CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, true) - @CompileManager.compile @project_id, @user_id, {}, (err, status)-> - status.should.equal "too-recently-compiled" - done() - - describe "should check the rate limit", -> - it "should return", (done)-> - @CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon.stub().callsArgWith(2, null, false) - @CompileManager.compile @project_id, @user_id, {}, (err, status)-> - status.should.equal "autocompile-backoff" - done() - - describe "getProjectCompileLimits", -> - beforeEach -> - @features = { - compileTimeout: @timeout = 42 - compileGroup: @group = "priority" - } - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project = { owner_ref: @owner_id = "owner-id-123" }) - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { features: @features }) - @CompileManager.getProjectCompileLimits @project_id, @callback - - it "should look up the owner of the project", -> - @ProjectGetter.getProject - .calledWith(@project_id, { owner_ref: 1 }) - .should.equal true - - it "should look up the owner's features", -> - @UserGetter.getUser - .calledWith(@project.owner_ref, { features: 1 }) - .should.equal true - - it "should return the limits", -> - @callback - .calledWith(null, { - timeout: @timeout - compileGroup: @group - }) - .should.equal true - - describe "deleteAuxFiles", -> - beforeEach -> - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" } - @ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3) - @CompileManager.deleteAuxFiles @project_id, @user_id, @callback - - it "should look up the compile group to use", -> - @CompileManager.getProjectCompileLimits - .calledWith(@project_id) - .should.equal true - - it "should delete the aux files", -> - @ClsiManager.deleteAuxFiles - .calledWith(@project_id, @user_id, @limits) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "_checkIfRecentlyCompiled", -> - describe "when the key exists in redis", -> - beforeEach -> - @rclient.set = sinon.stub().callsArgWith(5, null, null) - @CompileManager._checkIfRecentlyCompiled(@project_id, @user_id, @callback) - - it "should try to set the key", -> - @rclient.set - .calledWith("compile:#{@project_id}:#{@user_id}", true, "EX", @CompileManager.COMPILE_DELAY, "NX") - .should.equal true - - it "should call the callback with true", -> - @callback.calledWith(null, true).should.equal true - - describe "when the key does not exist in redis", -> - beforeEach -> - @rclient.set = sinon.stub().callsArgWith(5, null, "OK") - @CompileManager._checkIfRecentlyCompiled(@project_id, @user_id, @callback) - - it "should try to set the key", -> - @rclient.set - .calledWith("compile:#{@project_id}:#{@user_id}", true, "EX", @CompileManager.COMPILE_DELAY, "NX") - .should.equal true - - it "should call the callback with false", -> - @callback.calledWith(null, false).should.equal true - - describe "_checkIfAutoCompileLimitHasBeenHit", -> - - it "should be able to compile if it is not an autocompile", (done)-> - @ratelimiter.addCount.callsArgWith(2, null, true) - @CompileManager._checkIfAutoCompileLimitHasBeenHit false, "everyone", (err, canCompile)=> - canCompile.should.equal true - done() - - it "should be able to compile if rate limit has remianing", (done)-> - @ratelimiter.addCount.callsArgWith(1, null, true) - @CompileManager._checkIfAutoCompileLimitHasBeenHit true, "everyone", (err, canCompile)=> - args = @ratelimiter.addCount.args[0][0] - args.throttle.should.equal 25 - args.subjectName.should.equal "everyone" - args.timeInterval.should.equal 20 - args.endpointName.should.equal "auto_compile" - canCompile.should.equal true - done() - - it "should be not able to compile if rate limit has no remianing", (done)-> - @ratelimiter.addCount.callsArgWith(1, null, false) - @CompileManager._checkIfAutoCompileLimitHasBeenHit true, "everyone", (err, canCompile)=> - canCompile.should.equal false - done() - - it "should return false if there is an error in the rate limit", (done)-> - @ratelimiter.addCount.callsArgWith(1, "error") - @CompileManager._checkIfAutoCompileLimitHasBeenHit true, "everyone", (err, canCompile)=> - canCompile.should.equal false - done() - - describe "wordCount", -> - beforeEach -> - @CompileManager.getProjectCompileLimits = sinon.stub().callsArgWith 1, null, @limits = { compileGroup: "mock-compile-group" } - @ClsiManager.wordCount = sinon.stub().callsArg(4) - @CompileManager.wordCount @project_id, @user_id, false, @callback - - it "should look up the compile group to use", -> - @CompileManager.getProjectCompileLimits - .calledWith(@project_id) - .should.equal true - - it "should call wordCount for project", -> - @ClsiManager.wordCount - .calledWith(@project_id, @user_id, false, @limits) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true diff --git a/services/web/test/unit/coffee/Contact/ContactControllerTests.coffee b/services/web/test/unit/coffee/Contact/ContactControllerTests.coffee deleted file mode 100644 index 0f6c6a0404..0000000000 --- a/services/web/test/unit/coffee/Contact/ContactControllerTests.coffee +++ /dev/null @@ -1,67 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -assert = chai.assert -expect = chai.expect -modulePath = "../../../../app/js/Features/Contacts/ContactController.js" -SandboxedModule = require('sandboxed-module') - -describe "ContactController", -> - beforeEach -> - @AuthenticationController = - getLoggedInUserId: sinon.stub() - @ContactController = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } - "../User/UserGetter": @UserGetter = {} - "./ContactManager": @ContactManager = {} - "../Authentication/AuthenticationController": @AuthenticationController = {} - "../../infrastructure/Modules": @Modules = { hooks: {} } - '../Authentication/AuthenticationController': @AuthenticationController - - @next = sinon.stub() - @req = {} - @res = {} - @res.status = sinon.stub().returns @req - @res.send = sinon.stub() - - describe "getContacts", -> - beforeEach -> - @user_id = "mock-user-id" - @contact_ids = ["contact-1", "contact-2", "contact-3"] - @contacts = [ - { _id: "contact-1", email: "joe@example.com", first_name: "Joe", last_name: "Example", unsued: "foo" } - { _id: "contact-2", email: "jane@example.com", first_name: "Jane", last_name: "Example", unsued: "foo", holdingAccount: true } - { _id: "contact-3", email: "jim@example.com", first_name: "Jim", last_name: "Example", unsued: "foo" } - ] - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user_id) - @ContactManager.getContactIds = sinon.stub().callsArgWith(2, null, @contact_ids) - @UserGetter.getUsers = sinon.stub().callsArgWith(2, null, @contacts) - @Modules.hooks.fire = sinon.stub().callsArg(3) - - @ContactController.getContacts @req, @res, @next - - it "should look up the logged in user id", -> - @AuthenticationController.getLoggedInUserId - .calledWith(@req) - .should.equal true - - it "should get the users contact ids", -> - @ContactManager.getContactIds - .calledWith(@user_id, { limit: 50 }) - .should.equal true - - it "should populate the users contacts ids", -> - @UserGetter.getUsers - .calledWith(@contact_ids, { email: 1, first_name: 1, last_name: 1, holdingAccount: 1 }) - .should.equal true - - it "should fire the getContact module hook", -> - @Modules.hooks.fire - .calledWith("getContacts", @user_id) - .should.equal true - - it "should return a formatted list of contacts in contact list order, without holding accounts", -> - @res.send.args[0][0].contacts.should.deep.equal [ - { id: "contact-1", email: "joe@example.com", first_name: "Joe", last_name: "Example", type: "user" } - { id: "contact-3", email: "jim@example.com", first_name: "Jim", last_name: "Example", type: "user" } - ] diff --git a/services/web/test/unit/coffee/Contact/ContactManagerTests.coffee b/services/web/test/unit/coffee/Contact/ContactManagerTests.coffee deleted file mode 100644 index 054a7ec56f..0000000000 --- a/services/web/test/unit/coffee/Contact/ContactManagerTests.coffee +++ /dev/null @@ -1,91 +0,0 @@ -chai = require('chai') -chai.should() -sinon = require("sinon") -modulePath = "../../../../app/js/Features/Contacts/ContactManager" -SandboxedModule = require('sandboxed-module') - -describe "ContactManager", -> - beforeEach -> - @ContactManager = SandboxedModule.require modulePath, requires: - "request" : @request = sinon.stub() - "settings-sharelatex": @settings = - apis: - contacts: - url: "contacts.sharelatex.com" - "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub(), err:->} - - @user_id = "user-id-123" - @contact_id = "contact-id-123" - @callback = sinon.stub() - - describe "getContacts", -> - describe "with a successful response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, { contact_ids: @contact_ids = ["mock", "contact_ids"]}) - @ContactManager.getContactIds @user_id, { limit: 42 }, @callback - - it "should get the contacts from the contacts api", -> - @request.get - .calledWith({ - url: "#{@settings.apis.contacts.url}/user/#{@user_id}/contacts" - qs: { limit: 42 } - json: true - jar: false - }) - .should.equal true - - it "should call the callback with the contatcs", -> - @callback.calledWith(null, @contact_ids).should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 500, null) - @ContactManager.getContactIds @user_id, { limit: 42 }, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("contacts api responded with non-success code: 500")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Error("contacts api responded with a non-success code: 500") - user_id: @user_id - }, "error getting contacts for user") - .should.equal true - - describe "addContact", -> - describe "with a successful response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 200, null) - @ContactManager.addContact @user_id, @contact_id, @callback - - it "should add the contacts for the user in the contacts api", -> - @request.post - .calledWith({ - url: "#{@settings.apis.contacts.url}/user/#{@user_id}/contacts" - json: { - contact_id: @contact_id - } - jar: false - }) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500, null) - @ContactManager.addContact @user_id, @contact_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("contacts api responded with non-success code: 500")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Error("contacts api responded with a non-success code: 500") - user_id: @user_id - contact_id: @contact_id - }, "error adding contact for user") - .should.equal true diff --git a/services/web/test/unit/coffee/Cooldown/CooldownManagerTests.coffee b/services/web/test/unit/coffee/Cooldown/CooldownManagerTests.coffee deleted file mode 100644 index 96e27a8bf7..0000000000 --- a/services/web/test/unit/coffee/Cooldown/CooldownManagerTests.coffee +++ /dev/null @@ -1,120 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -expect = require('chai').expect -modulePath = require('path').join __dirname, '../../../../app/js/Features/Cooldown/CooldownManager' - - -describe "CooldownManager", -> - - beforeEach -> - @projectId = 'abcdefg' - @rclient = {set: sinon.stub(), get: sinon.stub()} - @RedisWrapper = - client: () => @rclient - @CooldownManager = SandboxedModule.require modulePath, requires: - '../../infrastructure/RedisWrapper': @RedisWrapper - 'logger-sharelatex': {log: sinon.stub()} - - describe '_buildKey', -> - - it 'should build a properly formatted redis key', -> - expect(@CooldownManager._buildKey('ABC')).to.equal 'Cooldown:{ABC}' - - describe 'isProjectOnCooldown', -> - beforeEach -> - @call = (cb) => - @CooldownManager.isProjectOnCooldown @projectId, cb - - describe 'when project is on cooldown', -> - beforeEach -> - @rclient.get = sinon.stub().callsArgWith(1, null, '1') - - it 'should fetch key from redis', (done) -> - @call (err, result) => - @rclient.get.callCount.should.equal 1 - @rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true - done() - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.equal null - done() - - it 'should produce a true result', (done) -> - @call (err, result) => - expect(result).to.equal true - done() - - describe 'when project is not on cooldown', -> - beforeEach -> - @rclient.get = sinon.stub().callsArgWith(1, null, null) - - it 'should fetch key from redis', (done) -> - @call (err, result) => - @rclient.get.callCount.should.equal 1 - @rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true - done() - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.equal null - done() - - it 'should produce a false result', (done) -> - @call (err, result) => - expect(result).to.equal false - done() - - describe 'when rclient.get produces an error', -> - beforeEach -> - @rclient.get = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should fetch key from redis', (done) -> - @call (err, result) => - @rclient.get.callCount.should.equal 1 - @rclient.get.calledWith('Cooldown:{abcdefg}').should.equal true - done() - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - describe 'putProjectOnCooldown', -> - - beforeEach -> - @call = (cb) => - @CooldownManager.putProjectOnCooldown @projectId, cb - - describe 'when rclient.set does not produce an error', -> - beforeEach -> - @rclient.set = sinon.stub().callsArgWith(4, null) - - it 'should set a key in redis', (done) -> - @call (err) => - @rclient.set.callCount.should.equal 1 - @rclient.set.calledWith('Cooldown:{abcdefg}').should.equal true - done() - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.equal null - done() - - describe 'when rclient.set produces an error', -> - beforeEach -> - @rclient.set = sinon.stub().callsArgWith(4, new Error('woops')) - - it 'should set a key in redis', (done) -> - @call (err) => - @rclient.set.callCount.should.equal 1 - @rclient.set.calledWith('Cooldown:{abcdefg}').should.equal true - done() - - it 'produce an error', (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() diff --git a/services/web/test/unit/coffee/Cooldown/CooldownMiddlewareTests.coffee b/services/web/test/unit/coffee/Cooldown/CooldownMiddlewareTests.coffee deleted file mode 100644 index 771ea65a88..0000000000 --- a/services/web/test/unit/coffee/Cooldown/CooldownMiddlewareTests.coffee +++ /dev/null @@ -1,88 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -expect = require('chai').expect -modulePath = require('path').join __dirname, '../../../../app/js/Features/Cooldown/CooldownMiddleware' - - -describe "CooldownMiddleware", -> - - beforeEach -> - @CooldownManager = - isProjectOnCooldown: sinon.stub() - @CooldownMiddleware = SandboxedModule.require modulePath, requires: - './CooldownManager': @CooldownManager - 'logger-sharelatex': {log: sinon.stub()} - - describe 'freezeProject', -> - - describe 'when project is on cooldown', -> - beforeEach -> - @CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, true) - @req = {params: {Project_id: 'abc'}} - @res = {sendStatus: sinon.stub()} - @next = sinon.stub() - - it 'should call CooldownManager.isProjectOnCooldown', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @CooldownManager.isProjectOnCooldown.callCount.should.equal 1 - @CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true - - it 'should not produce an error', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @next.callCount.should.equal 0 - - it 'should send a 429 status', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(429).should.equal true - - describe 'when project is not on cooldown', -> - beforeEach -> - @CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, false) - @req = {params: {Project_id: 'abc'}} - @res = {sendStatus: sinon.stub()} - @next = sinon.stub() - - it 'should call CooldownManager.isProjectOnCooldown', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @CooldownManager.isProjectOnCooldown.callCount.should.equal 1 - @CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true - - it 'call next with no arguments', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @next.callCount.should.equal 1 - expect(@next.lastCall.args.length).to.equal 0 - - describe 'when isProjectOnCooldown produces an error', -> - beforeEach -> - @CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, new Error('woops')) - @req = {params: {Project_id: 'abc'}} - @res = {sendStatus: sinon.stub()} - @next = sinon.stub() - - it 'should call CooldownManager.isProjectOnCooldown', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @CooldownManager.isProjectOnCooldown.callCount.should.equal 1 - @CooldownManager.isProjectOnCooldown.calledWith('abc').should.equal true - - it 'call next with an error', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - describe 'when projectId is not part of route', -> - beforeEach -> - @CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, true) - @req = {params: {lol: 'abc'}} - @res = {sendStatus: sinon.stub()} - @next = sinon.stub() - - it 'call next with an error', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should not call CooldownManager.isProjectOnCooldown', -> - @CooldownMiddleware.freezeProject @req, @res, @next - @CooldownManager.isProjectOnCooldown.callCount.should.equal 0 diff --git a/services/web/test/unit/coffee/Docstore/DocstoreManagerTests.coffee b/services/web/test/unit/coffee/Docstore/DocstoreManagerTests.coffee deleted file mode 100644 index ec7a8ce451..0000000000 --- a/services/web/test/unit/coffee/Docstore/DocstoreManagerTests.coffee +++ /dev/null @@ -1,293 +0,0 @@ -chai = require('chai') -chai.should() -sinon = require("sinon") -modulePath = "../../../../app/js/Features/Docstore/DocstoreManager" -SandboxedModule = require('sandboxed-module') -Errors = require "../../../../app/js/Features/Errors/Errors.js" - -describe "DocstoreManager", -> - beforeEach -> - @requestDefaults = sinon.stub().returns(@request = sinon.stub()) - @DocstoreManager = SandboxedModule.require modulePath, requires: - "request" : defaults: @requestDefaults - "settings-sharelatex": @settings = - apis: - docstore: - url: "docstore.sharelatex.com" - "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub(), err:->} - - @requestDefaults.calledWith(jar: false).should.equal true - - @project_id = "project-id-123" - @doc_id = "doc-id-123" - @callback = sinon.stub() - - describe "deleteDoc", -> - describe "with a successful response code", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, statusCode: 204, "") - @DocstoreManager.deleteDoc @project_id, @doc_id, @callback - - it "should delete the doc in the docstore api", -> - @request.del - .calledWith("#{@settings.apis.docstore.url}/project/#{@project_id}/doc/#{@doc_id}") - .should.equal true - - it "should call the callback without an error", -> - @callback.calledWith(null).should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, statusCode: 500, "") - @DocstoreManager.deleteDoc @project_id, @doc_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Error("docstore api responded with a non-success code: 500") - project_id: @project_id - doc_id: @doc_id - }, "error deleting doc in docstore") - .should.equal true - - describe "with a missing (404) response code", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, statusCode: 404, "") - @DocstoreManager.deleteDoc @project_id, @doc_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Errors.NotFoundError("tried to delete doc not in docstore")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Errors.NotFoundError("tried to delete doc not in docstore") - project_id: @project_id - doc_id: @doc_id - }, "tried to delete doc not in docstore") - .should.equal true - - describe "updateDoc", -> - beforeEach -> - @lines = ["mock", "doc", "lines"] - @rev = 5 - @version = 42 - @ranges = { "mock": "ranges" } - @modified = true - - describe "with a successful response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204, { modified: @modified, rev: @rev }) - @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @ranges, @callback - - it "should update the doc in the docstore api", -> - @request.post - .calledWith({ - url: "#{@settings.apis.docstore.url}/project/#{@project_id}/doc/#{@doc_id}" - json: - lines: @lines - version: @version - ranges: @ranges - }) - .should.equal true - - it "should call the callback with the modified status and revision", -> - @callback.calledWith(null, @modified, @rev).should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500, "") - @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @ranges, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Error("docstore api responded with a non-success code: 500") - project_id: @project_id - doc_id: @doc_id - }, "error updating doc in docstore") - .should.equal true - - describe "getDoc", -> - beforeEach -> - @doc = - lines: @lines = ["mock", "doc", "lines"] - rev: @rev = 5 - version: @version = 42 - ranges: @ranges = { "mock": "ranges" } - - describe "with a successful response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @doc) - @DocstoreManager.getDoc @project_id, @doc_id, @callback - - it "should get the doc from the docstore api", -> - @request.get - .calledWith({ - url: "#{@settings.apis.docstore.url}/project/#{@project_id}/doc/#{@doc_id}" - json: true - }) - .should.equal true - - it "should call the callback with the lines, version and rev", -> - @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 500, "") - @DocstoreManager.getDoc @project_id, @doc_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Error("docstore api responded with a non-success code: 500") - project_id: @project_id - doc_id: @doc_id - }, "error getting doc from docstore") - .should.equal true - - describe "with include_deleted=true", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @doc) - @DocstoreManager.getDoc @project_id, @doc_id, include_deleted: true, @callback - - it "should get the doc from the docstore api (including deleted)", -> - @request.get - .calledWith({ - url: "#{@settings.apis.docstore.url}/project/#{@project_id}/doc/#{@doc_id}?include_deleted=true" - json: true - }) - .should.equal true - - it "should call the callback with the lines, version and rev", -> - @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true - - describe "with a missing (404) response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 404, "") - @DocstoreManager.getDoc @project_id, @doc_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Errors.NotFoundError("doc not found in docstore")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Errors.NotFoundError("doc not found in docstore") - project_id: @project_id - doc_id: @doc_id - }, "doc not found in docstore") - .should.equal true - - describe "getAllDocs", -> - describe "with a successful response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @docs = [{ _id: "mock-doc-id" }]) - @DocstoreManager.getAllDocs @project_id, @callback - - it "should get all the project docs in the docstore api", -> - @request.get - .calledWith({ - url: "#{@settings.apis.docstore.url}/project/#{@project_id}/doc" - json: true - }) - .should.equal true - - it "should call the callback with the docs", -> - @callback.calledWith(null, @docs).should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 500, "") - @DocstoreManager.getAllDocs @project_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Error("docstore api responded with a non-success code: 500") - project_id: @project_id - }, "error getting all docs from docstore") - .should.equal true - - describe "getAllRanges", -> - describe "with a successful response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @docs = [{ _id: "mock-doc-id", ranges: "mock-ranges" }]) - @DocstoreManager.getAllRanges @project_id, @callback - - it "should get all the project doc ranges in the docstore api", -> - @request.get - .calledWith({ - url: "#{@settings.apis.docstore.url}/project/#{@project_id}/ranges" - json: true - }) - .should.equal true - - it "should call the callback with the docs", -> - @callback.calledWith(null, @docs).should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, statusCode: 500, "") - @DocstoreManager.getAllRanges @project_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true - - it "should log the error", -> - @logger.error - .calledWith({ - err: new Error("docstore api responded with a non-success code: 500") - project_id: @project_id - }, "error getting all doc ranges from docstore") - .should.equal true - - describe "archiveProject", -> - describe "with a successful response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204) - @DocstoreManager.archiveProject @project_id, @callback - - it "should call the callback", -> - @callback.called.should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500) - @DocstoreManager.archiveProject @project_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true - - - - describe "unarchiveProject", -> - describe "with a successful response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204) - @DocstoreManager.unarchiveProject @project_id, @callback - - it "should call the callback", -> - @callback.called.should.equal true - - describe "with a failed response code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500) - @DocstoreManager.unarchiveProject @project_id, @callback - - it "should call the callback with an error", -> - @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true - - diff --git a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee deleted file mode 100644 index 2965bc5a60..0000000000 --- a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee +++ /dev/null @@ -1,553 +0,0 @@ -should = require('chai').should() -spies = require('chai-spies') -chai = require('chai').use(spies) -sinon = require("sinon") -SandboxedModule = require('sandboxed-module') -assert = require('chai').assert -path = require 'path' -_ = require 'underscore' -ObjectId = require("mongojs").ObjectId; -modulePath = path.join __dirname, '../../../../app/js/Features/DocumentUpdater/DocumentUpdaterHandler' - -describe 'DocumentUpdaterHandler', -> - beforeEach -> - @project_id = "project-id-923" - @projectHistoryId = "ol-project-id-1" - @doc_id = "doc-id-394" - @lines = ["one", "two", "three"] - @version = 42 - @user_id = "mock-user-id-123" - @project = - _id: @project_id - - @request = sinon.stub() - @projectEntityHandler = {} - @settings = - apis: - documentupdater: - url : "http://document_updater.example.com" - project_history: - url: "http://project_history.example.com" - - @callback = sinon.stub() - @handler = SandboxedModule.require modulePath, requires: - 'request': defaults:=> return @request - 'settings-sharelatex':@settings - 'logger-sharelatex':{log:(->), error:(->), warn:(->)} - '../Project/ProjectEntityHandler':@projectEntityHandler - "../../models/Project": Project: @Project={} - '../../Features/Project/ProjectLocator':{} - "metrics-sharelatex": - Timer:-> - done:-> - - describe 'flushProjectToMongo', -> - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 204}, "") - @handler.flushProjectToMongo @project_id, @callback - - it 'should flush the document from the document updater', -> - @request.calledWithMatch( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/flush" - method: "POST" - ).should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.flushProjectToMongo @project_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.flushProjectToMongo @project_id, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe 'flushProjectToMongoAndDelete', -> - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 204}, "") - @handler.flushProjectToMongoAndDelete @project_id, @callback - - it 'should delete the project from the document updater', -> - @request.calledWithMatch( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}" - method: "DELETE" - ).should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.flushProjectToMongoAndDelete @project_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.flushProjectToMongoAndDelete @project_id, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe 'flushDocToMongo', -> - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 204}, "") - @handler.flushDocToMongo @project_id, @doc_id, @callback - - it 'should flush the document from the document updater', -> - @request.calledWithMatch( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/flush" - method: "POST" - ).should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.flushDocToMongo @project_id, @doc_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.flushDocToMongo @project_id, @doc_id, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe "deleteDoc", -> - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 204}, "") - @handler.deleteDoc @project_id, @doc_id, @callback - - it 'should delete the document from the document updater', -> - @request.calledWithMatch( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}" - method: "DELETE" - ).should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.deleteDoc @project_id, @doc_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.deleteDoc @project_id, @doc_id, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe "setDocument", -> - beforeEach -> - @source = "dropbox" - - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 204}, "") - @handler.setDocument @project_id, @doc_id, @user_id, @lines, @source, @callback - - it 'should set the document in the document updater', -> - @request.calledWith( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}" - json: - lines: @lines - source: @source - user_id: @user_id - method: "POST" - ).should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.setDocument @project_id, @doc_id, @user_id, @lines, @source, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.setDocument @project_id, @doc_id, @user_id, @lines, @source, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe "getDocument", -> - describe "successfully", -> - beforeEach -> - @body = - lines: @lines - version: @version - ops: @ops = ["mock-op-1", "mock-op-2"] - ranges: @ranges = {"mock":"ranges"} - @fromVersion = 2 - @request.callsArgWith(1, null, {statusCode: 200}, @body) - @handler.getDocument @project_id, @doc_id, @fromVersion, @callback - - it 'should get the document from the document updater', -> - @request.calledWith( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}?fromVersion=#{@fromVersion}" - method: "GET" - json: true - ).should.equal true - - it "should call the callback with the lines and version", -> - @callback.calledWith(null, @lines, @version, @ranges, @ops).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe "getProjectDocsIfMatch", -> - beforeEach -> - @project_state_hash = "1234567890abcdef" - - describe "successfully", -> - beforeEach -> - @doc0 = - _id: @doc_id - lines: @lines - v: @version - @docs = [ @doc0, @doc0, @doc0 ] - @body = JSON.stringify @docs - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body) - @handler.getProjectDocsIfMatch @project_id, @project_state_hash, @callback - - it 'should get the documents from the document updater', -> - url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/get_and_flush_if_old?state=#{@project_state_hash}" - @request.post.calledWith(url).should.equal true - - it "should call the callback with the documents", -> - @callback.calledWithExactly(null, @docs).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.getProjectDocsIfMatch @project_id, @project_state_hash, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a conflict error code", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, { statusCode: 409 }, "Conflict") - @handler.getProjectDocsIfMatch @project_id, @project_state_hash, @callback - - it "should return the callback with no documents", -> - @callback - .alwaysCalledWithExactly() - .should.equal true - - describe "clearProjectState", -> - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 200}) - @handler.clearProjectState @project_id, @callback - - it 'should clear the project state from the document updater', -> - @request.calledWithMatch( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/clearState" - method: "POST" - ).should.equal true - - it "should call the callback", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.clearProjectState @project_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns an error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, null) - @handler.clearProjectState @project_id, @callback - - it "should return the callback with no documents", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - - describe "acceptChanges", -> - beforeEach -> - @change_id = "mock-change-id-1" - - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 200}, @body) - @handler.acceptChanges @project_id, @doc_id, [ @change_id ], @callback - - it 'should accept the change in the document updater', -> - @request.calledWith( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/change/accept" - json: - change_ids: [ @change_id ] - method: "POST" - ).should.equal true - - it "should call the callback", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.acceptChanges @project_id, @doc_id, [ @change_id ], @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.acceptChanges @project_id, @doc_id, [ @change_id ], @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe "deleteThread", -> - beforeEach -> - @thread_id = "mock-thread-id-1" - - describe "successfully", -> - beforeEach -> - @request.callsArgWith(1, null, {statusCode: 200}, @body) - @handler.deleteThread @project_id, @doc_id, @thread_id, @callback - - it 'should delete the thread in the document updater', -> - @request.calledWithMatch( - url: "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/comment/#{@thread_id}" - method: "DELETE" - ).should.equal true - - it "should call the callback", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.callsArgWith(1, @error = new Error("something went wrong"), null, null) - @handler.deleteThread @project_id, @doc_id, @thread_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 500 }, "") - @handler.deleteThread @project_id, @doc_id, @thread_id, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("doc updater returned failure status code: 500")) - .should.equal true - - describe "updateProjectStructure ", -> - beforeEach -> - @user_id = 1234 - @version = 999 - - describe "with project history disabled", -> - beforeEach -> - @settings.apis.project_history.sendProjectStructureOps = false - @handler.updateProjectStructure @project_id, @projectHistoryId, @user_id, {}, @callback - - it 'does not make a web request', -> - @request.called.should.equal false - - it 'calls the callback', -> - @callback.called.should.equal true - - describe "with project history enabled", -> - beforeEach -> - @settings.apis.project_history.sendProjectStructureOps = true - @url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}" - @request.callsArgWith(1, null, {statusCode: 204}, "") - - describe "when an entity has changed name", -> - it 'should send the structure update to the document updater', (done) -> - @docIdA = new ObjectId() - @docIdB = new ObjectId() - @changes = { - oldDocs: [ - { path: '/old_a', doc: _id: @docIdA } - { path: '/old_b', doc: _id: @docIdB } - ] - # create new instances of the same ObjectIds so that == doesn't pass - newDocs: [ - { path: '/old_a', doc: _id: new ObjectId(@docIdA.toString()) } - { path: '/new_b', doc: _id: new ObjectId(@docIdB.toString()) } - ] - newProject: {version: @version} - } - - docUpdates = [ - id: @docIdB.toString(), - pathname: "/old_b" - newPathname: "/new_b" - ] - - @handler.updateProjectStructure @project_id, @projectHistoryId, @user_id, @changes, () => - @request.calledWith( - url: @url, - method: "POST" - json: {docUpdates, fileUpdates: [], userId: @user_id, @version, @projectHistoryId} - ) - .should.equal true - done() - - describe "when a doc has been added", -> - it 'should send the structure update to the document updater', (done) -> - @docId = new ObjectId() - @changes = { - newDocs: [ - { path: '/foo', docLines: 'a\nb', doc: _id: @docId } - ] - newProject: {version: @version} - } - - docUpdates = [ - id: @docId.toString(), - pathname: "/foo" - docLines: 'a\nb' - url: undefined - hash: undefined - ] - - @handler.updateProjectStructure @project_id, @projectHistoryId, @user_id, @changes, () => - @request.calledWith( - url: @url - method: "POST" - json: {docUpdates, fileUpdates: [], userId: @user_id, @version, @projectHistoryId} - ).should.equal true - done() - - describe "when a file has been added", -> - it 'should send the structure update to the document updater', (done) -> - @fileId = new ObjectId() - @changes = { - newFiles: [ - { path: '/bar', url: 'filestore.example.com/file', file: {_id: @fileId, hash: "12345" }} - ] - newProject: {version: @version} - } - - fileUpdates = [ - id: @fileId.toString(), - pathname: "/bar" - url: 'filestore.example.com/file' - docLines: undefined - hash: "12345" - ] - - @handler.updateProjectStructure @project_id, @projectHistoryId, @user_id, @changes, () => - @request.calledWith( - url: @url - method: "POST" - json: {docUpdates: [], fileUpdates, userId: @user_id, @version, @projectHistoryId} - ).should.equal true - done() - - describe "when an entity has been deleted", -> - it 'should end the structure update to the document updater', (done) -> - @docId = new ObjectId() - @changes = { - oldDocs: [ - { path: '/foo', docLines: 'a\nb', doc: _id: @docId } - ] - newProject: {version: @version} - } - - docUpdates = [ - id: @docId.toString(), - pathname: '/foo', - newPathname: '' - ] - - @handler.updateProjectStructure @project_id, @projectHistoryId, @user_id, @changes, () => - @request.calledWith( - url: @url - method: "POST" - json: {docUpdates, fileUpdates: [], userId: @user_id, @version, @projectHistoryId} - ).should.equal true - done() - - describe "when the project version is missing", -> - it 'should call the callback with an error', () -> - @docId = new ObjectId() - @changes = { - oldDocs: [ - { path: '/foo', docLines: 'a\nb', doc: _id: @docId } - ] - } - - docUpdates = [ - id: @docId.toString(), - pathname: '/foo', - newPathname: '' - ] - - @handler.updateProjectStructure @project_id, @projectHistoryId, @user_id, @changes, @callback - - @callback.calledWith(new Error()).should.equal true - firstCallArgs = @callback.args[0] - firstCallArgs[0].message.should.equal "did not receive project version in changes" \ No newline at end of file diff --git a/services/web/test/unit/coffee/Documents/DocumentControllerTests.coffee b/services/web/test/unit/coffee/Documents/DocumentControllerTests.coffee deleted file mode 100644 index 08cc7f1f83..0000000000 --- a/services/web/test/unit/coffee/Documents/DocumentControllerTests.coffee +++ /dev/null @@ -1,153 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Documents/DocumentController.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -Errors = require "../../../../app/js/Features/Errors/Errors" - -describe "DocumentController", -> - beforeEach -> - @DocumentController = SandboxedModule.require modulePath, requires: - "logger-sharelatex": - log:-> - err:-> - "../Project/ProjectGetter": @ProjectGetter = {} - "../Project/ProjectLocator": @ProjectLocator = {} - "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} - "../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {} - @res = new MockResponse() - @req = new MockRequest() - @next = sinon.stub() - @project_id = "project-id-123" - @doc_id = "doc-id-123" - @doc_lines = ["one", "two", "three"] - @version = 42 - @ranges = {"mock": "ranges"} - @pathname = '/a/b/c/file.tex' - @lastUpdatedAt = (new Date()).getTime() - @lastUpdatedBy = 'fake-last-updater-id' - @rev = 5 - - describe "getDocument", -> - beforeEach -> - @req.params = - Project_id: @project_id - doc_id: @doc_id - - describe "when the project exists without project history enabled", -> - beforeEach -> - @project = _id: @project_id - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - - describe "when the document exists", -> - beforeEach -> - @doc = _id: @doc_id - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @doc, fileSystem: @pathname) - @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @doc_lines, @rev, @version, @ranges) - @DocumentController.getDocument(@req, @res, @next) - - it "should get the project", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootFolder: true, overleaf: true) - .should.equal true - - it "should get the pathname of the document", -> - @ProjectLocator.findElement - .calledWith({project: @project, element_id: @doc_id, type: 'doc'}) - .should.equal true - - it "should get the document content", -> - @ProjectEntityHandler.getDoc - .calledWith(@project_id, @doc_id) - .should.equal true - - it "should return the document data to the client as JSON", -> - @res.type.should.equal "application/json" - @res.body.should.equal JSON.stringify - lines: @doc_lines - version: @version - ranges: @ranges - pathname: @pathname - - describe "when the document doesn't exist", -> - beforeEach -> - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, new Errors.NotFoundError("not found")) - @DocumentController.getDocument(@req, @res, @next) - - it "should call next with the NotFoundError", -> - @next.calledWith(new Errors.NotFoundError("not found")) - .should.equal true - - describe "when project exists with project history enabled", -> - beforeEach -> - @doc = _id: @doc_id - @projectHistoryId = 1234 - @project = _id: @project_id, overleaf: history: id: @projectHistoryId - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @doc, fileSystem: @pathname) - @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @doc_lines, @rev, @version, @ranges) - @DocumentController.getDocument(@req, @res, @next) - - it "should return the history id to the client as JSON", -> - @res.type.should.equal "application/json" - @res.body.should.equal JSON.stringify - lines: @doc_lines - version: @version - ranges: @ranges - pathname: @pathname - projectHistoryId: @projectHistoryId - - describe "when the project does not exist", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) - @DocumentController.getDocument(@req, @res, @next) - - it "returns a 404", -> - @res.statusCode.should.equal 404 - - describe "setDocument", -> - beforeEach -> - @req.params = - Project_id: @project_id - doc_id: @doc_id - - describe "when the document exists", -> - beforeEach -> - @ProjectEntityUpdateHandler.updateDocLines = sinon.stub().yields() - @req.body = - lines: @doc_lines - version: @version - ranges: @ranges - lastUpdatedAt: @lastUpdatedAt - lastUpdatedBy: @lastUpdatedBy - @DocumentController.setDocument(@req, @res, @next) - - it "should update the document in Mongo", -> - sinon.assert.calledWith( - @ProjectEntityUpdateHandler.updateDocLines, - @project_id, - @doc_id, - @doc_lines, - @version, - @ranges, - @lastUpdatedAt, - @lastUpdatedBy - ) - - it "should return a successful response", -> - @res.success.should.equal true - - describe "when the document doesn't exist", -> - beforeEach -> - @ProjectEntityUpdateHandler.updateDocLines = sinon.stub().yields(new Errors.NotFoundError("document does not exist")) - @req.body = - lines: @doc_lines - @DocumentController.setDocument(@req, @res, @next) - - it "should call next with the NotFoundError", -> - @next.calledWith(new Errors.NotFoundError("not found")) - .should.equal true diff --git a/services/web/test/unit/coffee/Documents/DocumentHelperTests.coffee b/services/web/test/unit/coffee/Documents/DocumentHelperTests.coffee deleted file mode 100644 index 28bcbe5439..0000000000 --- a/services/web/test/unit/coffee/Documents/DocumentHelperTests.coffee +++ /dev/null @@ -1,96 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Documents/DocumentHelper.js" -SandboxedModule = require('sandboxed-module') - -describe "DocumentHelper", -> - beforeEach -> - @DocumentHelper = SandboxedModule.require modulePath - - describe "getTitleFromTexContent", -> - - it "should return the title", -> - document = "\\begin{document}\n\\title{foo}\n\\end{document}" - expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "foo" - - it "should return the title if surrounded by space", -> - document = "\\begin{document}\n \\title{foo} \n\\end{document}" - expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "foo" - - it "should return null if there is no title", -> - document = "\\begin{document}\n\\end{document}" - expect(@DocumentHelper.getTitleFromTexContent(document)).to.eql null - - it "should accept an array", -> - document = ["\\begin{document}","\\title{foo}","\\end{document}"] - expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "foo" - - it "should parse out formatting elements from the title", -> - document = "\\title{\\textbf{\\large{Second Year LaTeX Exercise}}}" - expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "Second Year LaTeX Exercise" - - it "should ignore junk after the title", -> - document = "\\title{wombat} potato" - expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "wombat" - - it "should ignore junk before the title", -> - document = "% this is something that v1 relied on, even though it seems odd \\title{wombat}" - expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "wombat" - - # NICETOHAVE: Current implementation doesn't do this - #it "should keep content that surrounds formatting elements", -> - # document = "\\title{Second Year \\large{LaTeX} Exercise}" - # expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "Second Year LaTeX Exercise" - - it "should collapse whitespace", -> - document = "\\title{Second Year LaTeX Exercise}" - expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "Second Year LaTeX Exercise" - - describe "detex", -> - # note, there are a number of tests for getTitleFromTexContent that also test cases here - it 'leaves a non-TeX string unchanged', -> - expect(@DocumentHelper.detex('')).to.equal '' - expect(@DocumentHelper.detex('a')).to.equal 'a' - expect(@DocumentHelper.detex('a a')).to.equal 'a a' - - it 'collapses spaces', -> - expect(@DocumentHelper.detex('a a')).to.equal 'a a' - expect(@DocumentHelper.detex('a \n a')).to.equal 'a \n a' - - it 'replaces named commands', -> - expect(@DocumentHelper.detex('\\LaTeX')).to.equal 'LaTeX' - expect(@DocumentHelper.detex('\\TikZ')).to.equal 'TikZ' - expect(@DocumentHelper.detex('\\TeX')).to.equal 'TeX' - expect(@DocumentHelper.detex('\\BibTeX')).to.equal 'BibTeX' - - it 'removes general commands', -> - expect(@DocumentHelper.detex('\\foo')).to.equal '' - expect(@DocumentHelper.detex('\\foo{}')).to.equal '' - expect(@DocumentHelper.detex('\\foo~Test')).to.equal 'Test' - expect(@DocumentHelper.detex('\\"e')).to.equal 'e' - expect(@DocumentHelper.detex('\\textit{e}')).to.equal 'e' - - it 'leaves basic math', -> - expect(@DocumentHelper.detex('$\\cal{O}(n^2)$')).to.equal 'O(n^2)' - - it 'removes line spacing commands', -> - expect(@DocumentHelper.detex('a \\\\[1.50cm] b')).to.equal 'a b' - - describe "contentHasDocumentclass", -> - it "should return true if the content has a documentclass", -> - document = ["% line", "% line", "% line", "\\documentclass"] - expect(@DocumentHelper.contentHasDocumentclass(document)).to.equal true - - it "should allow whitespace before the documentclass", -> - document = ["% line", "% line", "% line", " \\documentclass"] - expect(@DocumentHelper.contentHasDocumentclass(document)).to.equal true - - it "should not allow non-whitespace before the documentclass", -> - document = ["% line", "% line", "% line", " asdf \\documentclass"] - expect(@DocumentHelper.contentHasDocumentclass(document)).to.equal false - - it "should return false when there is no documentclass", -> - document = ["% line", "% line", "% line"] - expect(@DocumentHelper.contentHasDocumentclass(document)).to.equal false diff --git a/services/web/test/unit/coffee/Downloads/ProjectDownloadsControllerTests.coffee b/services/web/test/unit/coffee/Downloads/ProjectDownloadsControllerTests.coffee deleted file mode 100644 index dfa21508d8..0000000000 --- a/services/web/test/unit/coffee/Downloads/ProjectDownloadsControllerTests.coffee +++ /dev/null @@ -1,124 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Downloads/ProjectDownloadsController.js" -SandboxedModule = require('sandboxed-module') -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" - -describe "ProjectDownloadsController", -> - beforeEach -> - @project_id = "project-id-123" - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @DocumentUpdaterHandler = sinon.stub() - @ProjectDownloadsController = SandboxedModule.require modulePath, requires: - "./ProjectZipStreamManager" : @ProjectZipStreamManager = {} - "../Project/ProjectGetter" : @ProjectGetter = {} - "metrics-sharelatex": @metrics = {} - "logger-sharelatex" : @logger = {log: sinon.stub()} - "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler - - describe "downloadProject", -> - beforeEach -> - @stream = - pipe: sinon.stub() - @ProjectZipStreamManager.createZipStreamForProject = - sinon.stub().callsArgWith(1, null, @stream) - @req.params = Project_id: @project_id - @res.contentType = sinon.stub() - @res.header = sinon.stub() - @project_name = "project name with accênts" - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, name: @project_name) - @DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1) - @metrics.inc = sinon.stub() - @ProjectDownloadsController.downloadProject @req, @res, @next - - it "should create a zip from the project", -> - @ProjectZipStreamManager.createZipStreamForProject - .calledWith(@project_id) - .should.equal true - - it "should stream the zip to the request", -> - @stream.pipe.calledWith(@res) - .should.equal true - - it "should set the correct content type on the request", -> - @res.contentType - .calledWith("application/zip") - .should.equal true - - it "should flush the project to mongo", -> - @DocumentUpdaterHandler.flushProjectToMongo - .calledWith(@project_id) - .should.equal true - - it "should look up the project's name", -> - @ProjectGetter.getProject - .calledWith(@project_id, name: true) - .should.equal(true) - - it "should name the downloaded file after the project", -> - @res.setContentDisposition - .calledWith( - 'attachment', - {filename: "#{@project_name}.zip"}) - .should.equal true - - it "should record the action via Metrics", -> - @metrics.inc.calledWith("zip-downloads").should.equal true - - it "should log the action", -> - @logger.log - .calledWith(sinon.match.any, "downloading project") - .should.equal true - - describe "downloadMultipleProjects", -> - beforeEach -> - @stream = - pipe: sinon.stub() - @ProjectZipStreamManager.createZipStreamForMultipleProjects = - sinon.stub().callsArgWith(1, null, @stream) - @project_ids = ["project-1", "project-2"] - @req.query = project_ids: @project_ids.join(",") - @res.contentType = sinon.stub() - @res.header = sinon.stub() - @DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon.stub().callsArgWith(1) - @metrics.inc = sinon.stub() - @ProjectDownloadsController.downloadMultipleProjects @req, @res, @next - - it "should create a zip from the project", -> - @ProjectZipStreamManager.createZipStreamForMultipleProjects - .calledWith(@project_ids) - .should.equal true - - it "should stream the zip to the request", -> - @stream.pipe.calledWith(@res) - .should.equal true - - it "should set the correct content type on the request", -> - @res.contentType - .calledWith("application/zip") - .should.equal true - - it "should flush the projects to mongo", -> - @DocumentUpdaterHandler.flushMultipleProjectsToMongo - .calledWith(@project_ids) - .should.equal true - - it "should name the downloaded file after the project", -> - @res.setContentDisposition - .calledWith( - 'attachment', - {filename: "Overleaf Projects (2 items).zip"}) - .should.equal true - - it "should record the action via Metrics", -> - @metrics.inc.calledWith("zip-downloads-multiple").should.equal true - - it "should log the action", -> - @logger.log - .calledWith(sinon.match.any, "downloading multiple projects") - .should.equal true diff --git a/services/web/test/unit/coffee/Downloads/ProjectZipStreamManagerTests.coffee b/services/web/test/unit/coffee/Downloads/ProjectZipStreamManagerTests.coffee deleted file mode 100644 index 72b8f38ead..0000000000 --- a/services/web/test/unit/coffee/Downloads/ProjectZipStreamManagerTests.coffee +++ /dev/null @@ -1,199 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Downloads/ProjectZipStreamManager.js" -SandboxedModule = require('sandboxed-module') -EventEmitter = require("events").EventEmitter - -describe "ProjectZipStreamManager", -> - beforeEach -> - @project_id = "project-id-123" - @callback = sinon.stub() - @archive = - on:-> - append: sinon.stub() - @ProjectZipStreamManager = SandboxedModule.require modulePath, requires: - "archiver": @archiver = sinon.stub().returns @archive - "logger-sharelatex": @logger = {error: sinon.stub(), log: sinon.stub()} - "../Project/ProjectEntityHandler" : @ProjectEntityHandler = {} - "../FileStore/FileStoreHandler": @FileStoreHandler = {} - '../Project/ProjectGetter': @ProjectGetter = {} - - - describe "createZipStreamForMultipleProjects", -> - describe "successfully", -> - beforeEach (done) -> - @project_ids = ["project-1", "project-2"] - @zip_streams = - "project-1": new EventEmitter() - "project-2": new EventEmitter() - - @project_names = - "project-1": "Project One Name" - "project-2": "Project Two Name" - - @ProjectZipStreamManager.createZipStreamForProject = (project_id, callback) => - callback null, @zip_streams[project_id] - setTimeout () => - @zip_streams[project_id].emit "end", - 0 - sinon.spy @ProjectZipStreamManager, "createZipStreamForProject" - - @ProjectGetter.getProject = (project_id, fields, callback) => - callback null, name: @project_names[project_id] - sinon.spy @ProjectGetter, "getProject" - - @ProjectZipStreamManager.createZipStreamForMultipleProjects @project_ids, (args...) => - @callback args... - - @archive.finalize = () -> - done() - - it "should create a zip archive", -> - @archiver.calledWith("zip").should.equal true - - it "should return a stream before any processing is done", -> - @callback.calledWith(sinon.match.falsy, @archive).should.equal true - @callback.calledBefore(@ProjectZipStreamManager.createZipStreamForProject).should.equal true - - it "should get a zip stream for all of the projects", -> - for project_id in @project_ids - @ProjectZipStreamManager.createZipStreamForProject - .calledWith(project_id) - .should.equal true - - it "should get the names of each project", -> - for project_id in @project_ids - @ProjectGetter.getProject - .calledWith(project_id, name: true) - .should.equal true - - it "should add all of the projects to the zip", -> - for project_id in @project_ids - @archive.append - .calledWith(@zip_streams[project_id], name: @project_names[project_id] + ".zip") - .should.equal true - - describe "createZipStreamForProject", -> - describe "successfully", -> - beforeEach -> - @ProjectZipStreamManager.addAllDocsToArchive = sinon.stub().callsArg(2) - @ProjectZipStreamManager.addAllFilesToArchive = sinon.stub().callsArg(2) - @archive.finalize = sinon.stub() - @ProjectZipStreamManager.createZipStreamForProject @project_id, @callback - - it "should create a zip archive", -> - @archiver.calledWith("zip").should.equal true - - it "should return a stream before any processing is done", -> - @callback.calledWith(sinon.match.falsy, @archive).should.equal true - @callback.calledBefore(@ProjectZipStreamManager.addAllDocsToArchive).should.equal true - @callback.calledBefore(@ProjectZipStreamManager.addAllFilesToArchive).should.equal true - - it "should add all of the project docs to the zip", -> - @ProjectZipStreamManager.addAllDocsToArchive - .calledWith(@project_id, @archive) - .should.equal true - - it "should add all of the project files to the zip", -> - @ProjectZipStreamManager.addAllFilesToArchive - .calledWith(@project_id, @archive) - .should.equal true - - it "should finalise the stream", -> - @archive.finalize.called.should.equal true - - describe "with an error adding docs", -> - beforeEach -> - @ProjectZipStreamManager.addAllDocsToArchive = - sinon.stub().callsArgWith(2, new Error("something went wrong")) - @ProjectZipStreamManager.addAllFilesToArchive = sinon.stub().callsArg(2) - @archive.finalize = sinon.stub() - @ProjectZipStreamManager.createZipStreamForProject @project_id, @callback - - it "should log out an error", -> - @logger.error.calledWith(sinon.match.any, "error adding docs to zip stream") - .should.equal true - - it "should continue with the process", -> - @ProjectZipStreamManager.addAllDocsToArchive.called.should.equal true - @ProjectZipStreamManager.addAllFilesToArchive.called.should.equal true - @archive.finalize.called.should.equal true - - describe "with an error adding files", -> - beforeEach -> - @ProjectZipStreamManager.addAllDocsToArchive = sinon.stub().callsArg(2) - @ProjectZipStreamManager.addAllFilesToArchive = - sinon.stub().callsArgWith(2, new Error("something went wrong")) - @archive.finalize = sinon.stub() - @ProjectZipStreamManager.createZipStreamForProject @project_id, @callback - - it "should log out an error", -> - @logger.error.calledWith(sinon.match.any, "error adding files to zip stream") - .should.equal true - - it "should continue with the process", -> - @ProjectZipStreamManager.addAllDocsToArchive.called.should.equal true - @ProjectZipStreamManager.addAllFilesToArchive.called.should.equal true - @archive.finalize.called.should.equal true - - describe "addAllDocsToArchive", -> - beforeEach (done) -> - @docs = - "/main.tex": - lines: ["\\documentclass{article}", "\\begin{document}", "Hello world", "\\end{document}"] - "/chapters/chapter1.tex": - lines: ["chapter1", "content"] - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectZipStreamManager.addAllDocsToArchive @project_id, @archive, (error) => - @callback(error) - done() - - it "should get the docs for the project", -> - @ProjectEntityHandler.getAllDocs - .calledWith(@project_id) - .should.equal true - - it "should add each doc to the archive", -> - for path, doc of @docs - path = path.slice(1) # remove "/" - @archive.append.calledWith(doc.lines.join("\n"), name: path) - .should.equal true - - describe "addAllFilesToArchive", -> - beforeEach -> - @files = - "/image.png": - _id: "file-id-1" - "/folder/picture.png": - _id: "file-id-2" - @streams = - "file-id-1" : new EventEmitter() - "file-id-2" : new EventEmitter() - @ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files) - @FileStoreHandler.getFileStream = (project_id, file_id, {}, callback) => - callback null, @streams[file_id] - sinon.spy @FileStoreHandler, "getFileStream" - @ProjectZipStreamManager.addAllFilesToArchive @project_id, @archive, @callback - for path, stream of @streams - stream.emit "end" - - it "should get the files for the project", -> - @ProjectEntityHandler.getAllFiles.calledWith(@project_id).should.equal true - - it "should get a stream for each file", -> - for path, file of @files - @FileStoreHandler.getFileStream.calledWith(@project_id, file._id).should.equal true - - it "should add each file to the archive", -> - for path, file of @files - path = path.slice(1) # remove "/" - @archive.append.calledWith(@streams[file._id], name: path).should.equal true - - - - - - - diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee deleted file mode 100644 index c9cad856e6..0000000000 --- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee +++ /dev/null @@ -1,475 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -expect = require("chai").expect - -modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor/EditorController' -MockClient = require "../helpers/MockClient" -assert = require('assert') - -describe "EditorController", -> - beforeEach -> - @project_id = "test-project-id" - @source = "dropbox" - - @doc = _id: @doc_id = "test-doc-id" - @docName = "doc.tex" - @docLines = ["1234","dskl"] - @file = _id: @file_id ="dasdkjk" - @fileName = "file.png" - @fsPath = "/folder/file.png" - @linkedFileData = {provider: 'url'} - - @newFile = _id: "new-file-id" - - @folder_id = "123ksajdn" - @folder = _id: @folder_id - @folderName = "folder" - - @callback = sinon.stub() - - @EditorController = SandboxedModule.require modulePath, requires: - '../Project/ProjectEntityUpdateHandler' : @ProjectEntityUpdateHandler = {} - '../Project/ProjectOptionsHandler' : @ProjectOptionsHandler = - setCompiler: sinon.stub().yields() - setImageName: sinon.stub().yields() - setSpellCheckLanguage: sinon.stub().yields() - '../Project/ProjectDetailsHandler': @ProjectDetailsHandler = - setProjectDescription: sinon.stub().yields() - renameProject: sinon.stub().yields() - setPublicAccessLevel: sinon.stub().yields() - '../Project/ProjectDeleter' : @ProjectDeleter = {} - '../DocumentUpdater/DocumentUpdaterHandler' : @DocumentUpdaterHandler = - flushDocToMongo: sinon.stub().yields() - setDocument: sinon.stub().yields() - './EditorRealTimeController':@EditorRealTimeController = - emitToRoom: sinon.stub() - "metrics-sharelatex": @Metrics = inc: sinon.stub() - "logger-sharelatex": @logger = - log: sinon.stub() - err: sinon.stub() - - describe 'addDoc', -> - beforeEach -> - @ProjectEntityUpdateHandler.addDocWithRanges = sinon.stub().yields(null, @doc, @folder_id) - @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, @callback - - it 'should add the doc using the project entity handler', -> - @ProjectEntityUpdateHandler.addDocWithRanges - .calledWith(@project_id, @folder_id, @docName, @docLines, {}) - .should.equal true - - it 'should send the update out to the users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source) - .should.equal true - - it 'calls the callback', -> - @callback.calledWith(null, @doc).should.equal true - - describe 'addFile', -> - beforeEach -> - @ProjectEntityUpdateHandler.addFile = sinon.stub().yields(null, @file, @folder_id) - @EditorController.addFile @project_id, @folder_id, @fileName, @fsPath, @linkedFileData, @source, @user_id, @callback - - it 'should add the folder using the project entity handler', -> - @ProjectEntityUpdateHandler.addFile - .calledWith(@project_id, @folder_id, @fileName, @fsPath, @linkedFileData, @user_id) - .should.equal true - - it 'should send the update of a new folder out to the users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source, @linkedFileData) - .should.equal true - - it 'calls the callback', -> - @callback.calledWith(null, @file).should.equal true - - describe 'upsertDoc', -> - beforeEach -> - @ProjectEntityUpdateHandler.upsertDoc = sinon.stub().yields(null, @doc, false) - @EditorController.upsertDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, @callback - - it 'upserts the doc using the project entity handler', -> - @ProjectEntityUpdateHandler.upsertDoc - .calledWith(@project_id, @folder_id, @docName, @docLines, @source) - .should.equal true - - it 'returns the doc', -> - @callback.calledWith(null, @doc).should.equal true - - describe 'doc does not exist', -> - beforeEach -> - @ProjectEntityUpdateHandler.upsertDoc = sinon.stub().yields(null, @doc, true) - @EditorController.upsertDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, @callback - - it 'sends an update out to users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source) - .should.equal true - - describe 'upsertFile', -> - beforeEach -> - @ProjectEntityUpdateHandler.upsertFile = sinon.stub().yields(null, @newFile, false, @file) - @EditorController.upsertFile @project_id, @folder_id, @fileName, @fsPath, @linkedFileData, @source, @user_id, @callback - - it 'upserts the file using the project entity handler', -> - @ProjectEntityUpdateHandler.upsertFile - .calledWith(@project_id, @folder_id, @fileName, @fsPath, @linkedFileData, @user_id) - .should.equal true - - it 'returns the file', -> - @callback.calledWith(null, @newFile).should.equal true - - describe 'file does not exist', -> - beforeEach -> - @ProjectEntityUpdateHandler.upsertFile = sinon.stub().yields(null, @file, true) - @EditorController.upsertFile @project_id, @folder_id, @fileName, @fsPath, @linkedFileData, @source, @user_id, @callback - - it 'should send the update out to users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source, @linkedFileData) - .should.equal true - - describe "upsertDocWithPath", -> - beforeEach -> - @docPath = '/folder/doc' - - @ProjectEntityUpdateHandler.upsertDocWithPath = sinon.stub().yields(null, @doc, false, [], @folder) - @EditorController.upsertDocWithPath @project_id, @docPath, @docLines, @source, @user_id, @callback - - it 'upserts the doc using the project entity handler', -> - @ProjectEntityUpdateHandler.upsertDocWithPath - .calledWith(@project_id, @docPath, @docLines, @source) - .should.equal true - - describe 'doc does not exist', -> - beforeEach -> - @ProjectEntityUpdateHandler.upsertDocWithPath = sinon.stub().yields(null, @doc, true, [], @folder) - @EditorController.upsertDocWithPath @project_id, @docPath, @docLines, @source, @user_id, @callback - - it 'should send the update for the doc out to users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source) - .should.equal true - - describe 'folders required for doc do not exist', -> - beforeEach -> - folders = [ - @folderA = { _id: 2, parentFolder_id: 1} - @folderB = { _id: 3, parentFolder_id: 2} - ] - @ProjectEntityUpdateHandler.upsertDocWithPath = sinon.stub().yields(null, @doc, true, folders, @folderB) - @EditorController.upsertDocWithPath @project_id, @docPath, @docLines, @source, @user_id, @callback - - it 'should send the update for each folder to users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFolder", @folderA.parentFolder_id, @folderA) - .should.equal true - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFolder", @folderB.parentFolder_id, @folderB) - .should.equal true - - describe "upsertFileWithPath", -> - beforeEach -> - @filePath = '/folder/file' - - @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @newFile, false, @file, [], @folder) - @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @linkedFileData, @source, @user_id, @callback - - it 'upserts the file using the project entity handler', -> - @ProjectEntityUpdateHandler.upsertFileWithPath - .calledWith(@project_id, @filePath, @fsPath, @linkedFileData) - .should.equal true - - describe 'file does not exist', -> - beforeEach -> - @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, undefined, [], @folder) - @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @linkedFileData, @source, @user_id, @callback - - it 'should send the update for the file out to users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source, @linkedFileData) - .should.equal true - - describe 'folders required for file do not exist', -> - beforeEach -> - folders = [ - @folderA = { _id: 2, parentFolder_id: 1} - @folderB = { _id: 3, parentFolder_id: 2} - ] - @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, undefined, folders, @folderB) - @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @linkedFileData, @source, @user_id, @callback - - it 'should send the update for each folder to users in the project', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFolder", @folderA.parentFolder_id, @folderA) - .should.equal true - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFolder", @folderB.parentFolder_id, @folderB) - .should.equal true - - describe 'addFolder', -> - beforeEach -> - @EditorController._notifyProjectUsersOfNewFolder = sinon.stub().yields() - @ProjectEntityUpdateHandler.addFolder = sinon.stub().yields(null, @folder, @folder_id) - @EditorController.addFolder @project_id, @folder_id, @folderName, @source, @callback - - it 'should add the folder using the project entity handler', -> - @ProjectEntityUpdateHandler.addFolder - .calledWith(@project_id, @folder_id, @folderName) - .should.equal true - - it 'should notifyProjectUsersOfNewFolder', -> - @EditorController._notifyProjectUsersOfNewFolder - .calledWith(@project_id, @folder_id, @folder) - - it 'should return the folder in the callback', -> - @callback.calledWith(null, @folder).should.equal true - - describe 'mkdirp', -> - beforeEach -> - @path = "folder1/folder2" - @folders = [ - @folderA = { _id: 2, parentFolder_id: 1} - @folderB = { _id: 3, parentFolder_id: 2} - ] - @EditorController._notifyProjectUsersOfNewFolders = sinon.stub().yields() - @ProjectEntityUpdateHandler.mkdirp = sinon.stub().yields(null, @folders, @folder) - @EditorController.mkdirp @project_id, @path, @callback - - it 'should create the folder using the project entity handler', -> - @ProjectEntityUpdateHandler.mkdirp - .calledWith(@project_id, @path) - .should.equal true - - it 'should notifyProjectUsersOfNewFolder', -> - @EditorController._notifyProjectUsersOfNewFolders - .calledWith(@project_id, @folders) - - it 'should return the folder in the callback', -> - @callback.calledWith(null, @folders, @folder).should.equal true - - describe 'deleteEntity', -> - beforeEach -> - @entity_id = "entity_id_here" - @type = "doc" - @ProjectEntityUpdateHandler.deleteEntity = sinon.stub().yields() - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, @callback - - it 'should delete the folder using the project entity handler', -> - @ProjectEntityUpdateHandler.deleteEntity - .calledWith(@project_id, @entity_id, @type, @user_id) - .should.equal.true - - it 'notify users an entity has been deleted', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "removeEntity", @entity_id, @source) - .should.equal true - - describe "deleteEntityWithPath", -> - beforeEach () -> - @entity_id = "entity_id_here" - @ProjectEntityUpdateHandler.deleteEntityWithPath = sinon.stub().yields(null, @entity_id) - @path = "folder1/folder2" - @EditorController.deleteEntityWithPath @project_id, @path, @source, @user_id, @callback - - it 'should delete the folder using the project entity handler', -> - @ProjectEntityUpdateHandler.deleteEntityWithPath - .calledWith(@project_id, @path, @user_id) - .should.equal.true - - it 'notify users an entity has been deleted', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "removeEntity", @entity_id, @source) - .should.equal true - - describe "notifyUsersProjectHasBeenDeletedOrRenamed", -> - it 'should emmit a message to all users in a project', (done)-> - @EditorController.notifyUsersProjectHasBeenDeletedOrRenamed @project_id, (err)=> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "projectRenamedOrDeletedByExternalSource") - .should.equal true - done() - - describe "updateProjectDescription", -> - beforeEach -> - @description = "new description" - @EditorController.updateProjectDescription @project_id, @description, @callback - - it "should send the new description to the project details handler", -> - @ProjectDetailsHandler.setProjectDescription.calledWith(@project_id, @description).should.equal true - - it "should notify the other clients about the updated description", -> - @EditorRealTimeController.emitToRoom.calledWith(@project_id, "projectDescriptionUpdated", @description).should.equal true - - describe "deleteProject", -> - beforeEach -> - @err = "errro" - @ProjectDeleter.deleteProject = sinon.stub().callsArgWith(1, @err) - - it "should call the project handler", (done)-> - @EditorController.deleteProject @project_id, (err)=> - err.should.equal @err - @ProjectDeleter.deleteProject.calledWith(@project_id).should.equal true - done() - - describe "renameEntity", -> - beforeEach (done) -> - @entity_id = "entity_id_here" - @entityType = "doc" - @newName = "bobsfile.tex" - @ProjectEntityUpdateHandler.renameEntity = sinon.stub().yields() - - @EditorController.renameEntity @project_id, @entity_id, @entityType, @newName, @user_id, done - - it "should call the project handler", -> - @ProjectEntityUpdateHandler.renameEntity - .calledWith(@project_id, @entity_id, @entityType, @newName, @user_id) - .should.equal true - - it "should emit the update to the room", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'reciveEntityRename', @entity_id, @newName) - .should.equal true - - describe "moveEntity", -> - beforeEach -> - @entity_id = "entity_id_here" - @entityType = "doc" - @ProjectEntityUpdateHandler.moveEntity = sinon.stub().yields() - @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, @user_id, @callback - - it "should call the ProjectEntityUpdateHandler", -> - @ProjectEntityUpdateHandler.moveEntity - .calledWith(@project_id, @entity_id, @folder_id, @entityType, @user_id) - .should.equal true - - it "should emit the update to the room", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'reciveEntityMove', @entity_id, @folder_id) - .should.equal true - - it "calls the callback", -> - @callback.called.should.equal true - - describe "renameProject", -> - beforeEach -> - @err = "errro" - @newName = "new name here" - @EditorController.renameProject @project_id, @newName, @callback - - it "should call the EditorController", -> - @ProjectDetailsHandler.renameProject.calledWith(@project_id, @newName).should.equal true - - it "should emit the update to the room", -> - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'projectNameUpdated', @newName).should.equal true - - describe "setCompiler", -> - beforeEach -> - @compiler = "latex" - @EditorController.setCompiler @project_id, @compiler, @callback - - it "should send the new compiler and project id to the project options handler", -> - @ProjectOptionsHandler.setCompiler - .calledWith(@project_id, @compiler) - .should.equal true - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "compilerUpdated", @compiler) - .should.equal true - - describe "setImageName", -> - beforeEach -> - @imageName = "texlive-1234.5" - @EditorController.setImageName @project_id, @imageName, @callback - - it "should send the new imageName and project id to the project options handler", -> - @ProjectOptionsHandler.setImageName - .calledWith(@project_id, @imageName) - .should.equal true - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "imageNameUpdated", @imageName) - .should.equal true - - describe "setSpellCheckLanguage", -> - beforeEach -> - @languageCode = "fr" - @EditorController.setSpellCheckLanguage @project_id, @languageCode, @callback - - it "should send the new languageCode and project id to the project options handler", -> - @ProjectOptionsHandler.setSpellCheckLanguage - .calledWith(@project_id, @languageCode) - .should.equal true - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "spellCheckLanguageUpdated", @languageCode) - .should.equal true - - describe "setPublicAccessLevel", -> - describe 'when setting to private', -> - beforeEach -> - @newAccessLevel = 'private' - @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields(null, @tokens) - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, @callback - - it 'should set the access level', -> - @ProjectDetailsHandler.setPublicAccessLevel - .calledWith(@project_id, @newAccessLevel) - .should.equal true - - it 'should broadcast the access level change', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:publicAccessLevel:changed') - .should.equal true - - it 'should not ensure tokens are present for project', -> - @ProjectDetailsHandler.ensureTokensArePresent - .calledWith(@project_id) - .should.equal false - - it 'should not broadcast a token change', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) - .should.equal false - - describe 'when setting to tokenBased', -> - beforeEach -> - @newAccessLevel = 'tokenBased' - @tokens = {readOnly: 'aaa', readAndWrite: '42bbb'} - @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields(null, @tokens) - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, @callback - - it 'should set the access level', -> - @ProjectDetailsHandler.setPublicAccessLevel - .calledWith(@project_id, @newAccessLevel) - .should.equal true - - it 'should broadcast the access level change', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:publicAccessLevel:changed') - .should.equal true - - it 'should ensure tokens are present for project', -> - @ProjectDetailsHandler.ensureTokensArePresent - .calledWith(@project_id) - .should.equal true - - it 'should broadcast the token change too', -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) - .should.equal true - - describe "setRootDoc", -> - beforeEach -> - @newRootDocID = "21312321321" - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() - @EditorController.setRootDoc @project_id, @newRootDocID, @callback - - it "should call the ProjectEntityUpdateHandler", -> - @ProjectEntityUpdateHandler.setRootDoc - .calledWith(@project_id, @newRootDocID) - .should.equal true - - it "should emit the update to the room", -> - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'rootDocUpdated', @newRootDocID) - .should.equal true diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee deleted file mode 100644 index 01f6b17d39..0000000000 --- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee +++ /dev/null @@ -1,309 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor/EditorHttpController' - -describe "EditorHttpController", -> - beforeEach -> - @EditorHttpController = SandboxedModule.require modulePath, requires: - '../Project/ProjectEntityUpdateHandler' : @ProjectEntityUpdateHandler = {} - '../Project/ProjectDeleter' : @ProjectDeleter = {} - '../Project/ProjectGetter' : @ProjectGetter = {} - '../User/UserGetter' : @UserGetter = {} - "../Authorization/AuthorizationManager": @AuthorizationManager = {} - '../Project/ProjectEditorHandler': @ProjectEditorHandler = {} - "./EditorRealTimeController": @EditorRealTimeController = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } - "./EditorController": @EditorController = {} - 'metrics-sharelatex': @Metrics = {inc: sinon.stub()} - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} - "../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} - "../TokenAccess/TokenAccessHandler": @TokenAccessHandler = {} - "../Authentication/AuthenticationController": @AuthenticationController = {} - - @project_id = "mock-project-id" - @doc_id = "mock-doc-id" - @user_id = "mock-user-id" - @parent_folder_id = "mock-folder-id" - @userId = 1234 - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId) - @req = {} - @res = - send: sinon.stub() - sendStatus: sinon.stub() - json: sinon.stub() - @callback = sinon.stub() - @TokenAccessHandler.getRequestToken = sinon.stub().returns(@token = null) - @TokenAccessHandler.protectTokens = sinon.stub() - - describe "joinProject", -> - beforeEach -> - @req.params = - Project_id: @project_id - @req.query = - user_id: @user_id - @projectView = { - _id: @project_id - } - @EditorHttpController._buildJoinProjectView = sinon.stub().callsArgWith(3, null, @projectView, "owner") - @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() - - describe "successfully", -> - beforeEach -> - @EditorHttpController.joinProject @req, @res - - it "should get the project view", -> - @EditorHttpController._buildJoinProjectView - .calledWith(@req, @project_id, @user_id) - .should.equal true - - it "should return the project and privilege level", -> - @res.json - .calledWith({ - project: @projectView - privilegeLevel: "owner" - }) - .should.equal true - - it "should not try to unmark the project as deleted", -> - @ProjectDeleter.unmarkAsDeletedByExternalSource - .called - .should.equal false - - it "should send an inc metric", -> - @Metrics.inc - .calledWith("editor.join-project") - .should.equal true - - describe "when the project is marked as deleted", -> - beforeEach -> - @projectView.deletedByExternalDataSource = true - @EditorHttpController.joinProject @req, @res - - it "should unmark the project as deleted", -> - @ProjectDeleter.unmarkAsDeletedByExternalSource - .calledWith(@project_id) - .should.equal true - - describe "with an anonymous user", -> - beforeEach -> - @req.query = - user_id: "anonymous-user" - @EditorHttpController.joinProject @req, @res - - it "should pass the user id as null", -> - @EditorHttpController._buildJoinProjectView - .calledWith(@req, @project_id, null) - .should.equal true - - describe "_buildJoinProjectView", -> - beforeEach -> - @project = - _id: @project_id - owner_ref:{_id:"something"} - @user = - _id: @user_id = "user-id" - projects: {} - @members = ["members", "mock"] - @tokenMembers = ['one', 'two'] - @projectModelView = - _id: @project_id - owner:{_id:"something"} - view: true - @invites = [ - {_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id} - {_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id} - ] - @ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView) - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) - @CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members) - @CollaboratorsHandler.getTokenMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @tokenMembers) - @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @invites) - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) - - describe "when authorized", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(3, null, "owner") - @EditorHttpController._buildJoinProjectView(@req, @project_id, @user_id, @callback) - - it "should find the project without doc lines", -> - @ProjectGetter.getProjectWithoutDocLines - .calledWith(@project_id) - .should.equal true - - it "should get the list of users in the project", -> - @CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels - .calledWith(@project_id) - .should.equal true - - it "should check the privilege level", -> - @AuthorizationManager.getPrivilegeLevelForProject - .calledWith(@user_id, @project_id, @token) - .should.equal true - - it 'should include the invites', -> - @CollaboratorsInviteHandler.getAllInvites - .calledWith(@project._id) - .should.equal true - - it "should return the project model view, privilege level and protocol version", -> - @callback.calledWith(null, @projectModelView, "owner").should.equal true - - describe "when not authorized", -> - beforeEach -> - @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(3, null, null) - @EditorHttpController._buildJoinProjectView(@req, @project_id, @user_id, @callback) - - it "should return false in the callback", -> - @callback.calledWith(null, null, false).should.equal true - - describe "addDoc", -> - beforeEach -> - @doc = { "mock": "doc" } - @req.params = - Project_id: @project_id - @req.body = - name: @name = "doc-name" - parent_folder_id: @parent_folder_id - @EditorController.addDoc = sinon.stub().callsArgWith(6, null, @doc) - - describe "successfully", -> - beforeEach -> - @EditorHttpController.addDoc @req, @res - - it "should call EditorController.addDoc", -> - @EditorController.addDoc - .calledWith(@project_id, @parent_folder_id, @name, [], "editor", @userId) - .should.equal true - - it "should send the doc back as JSON", -> - @res.json - .calledWith(@doc) - .should.equal true - - describe "unsuccesfully", -> - beforeEach -> - @req.body.name = "" - @EditorHttpController.addDoc @req, @res - - it "should send back a bad request status code", -> - @res.sendStatus.calledWith(400).should.equal true - - describe "addFolder", -> - beforeEach -> - @folder = { "mock": "folder" } - @req.params = - Project_id: @project_id - @req.body = - name: @name = "folder-name" - parent_folder_id: @parent_folder_id - @EditorController.addFolder = sinon.stub().callsArgWith(4, null, @folder) - - describe "successfully", -> - beforeEach -> - @EditorHttpController.addFolder @req, @res - - it "should call EditorController.addFolder", -> - @EditorController.addFolder - .calledWith(@project_id, @parent_folder_id, @name, "editor") - .should.equal true - - it "should send the folder back as JSON", -> - @res.json - .calledWith(@folder) - .should.equal true - - describe "unsuccesfully", -> - - beforeEach -> - @req.body.name = "" - @EditorHttpController.addFolder @req, @res - - it "should send back a bad request status code", -> - @res.sendStatus.calledWith(400).should.equal true - - - describe "renameEntity", -> - beforeEach -> - @req.params = - Project_id: @project_id - entity_id: @entity_id = "entity-id-123" - entity_type: @entity_type = "entity-type" - @req.body = - name: @name = "new-name" - @EditorController.renameEntity = sinon.stub().callsArg(5) - @EditorHttpController.renameEntity @req, @res - - it "should call EditorController.renameEntity", -> - @EditorController.renameEntity - .calledWith(@project_id, @entity_id, @entity_type, @name, @userId) - .should.equal true - - it "should send back a success response", -> - @res.sendStatus.calledWith(204).should.equal true - - describe "renameEntity with long name", -> - beforeEach -> - @req.params = - Project_id: @project_id - entity_id: @entity_id = "entity-id-123" - entity_type: @entity_type = "entity-type" - @req.body = - name: @name = "EDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOTEDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOT" - @EditorController.renameEntity = sinon.stub().callsArg(4) - @EditorHttpController.renameEntity @req, @res - - it "should send back a bad request status code", -> - @res.sendStatus.calledWith(400).should.equal true - - describe "rename entity with 0 length name", -> - beforeEach -> - @req.params = - Project_id: @project_id - entity_id: @entity_id = "entity-id-123" - entity_type: @entity_type = "entity-type" - @req.body = - name: @name = "" - @EditorController.renameEntity = sinon.stub().callsArg(4) - @EditorHttpController.renameEntity @req, @res - - it "should send back a bad request status code", -> - @res.sendStatus.calledWith(400).should.equal true - - describe "moveEntity", -> - beforeEach -> - @req.params = - Project_id: @project_id - entity_id: @entity_id = "entity-id-123" - entity_type: @entity_type = "entity-type" - @req.body = - folder_id: @folder_id = "folder-id-123" - @EditorController.moveEntity = sinon.stub().callsArg(5) - @EditorHttpController.moveEntity @req, @res - - it "should call EditorController.moveEntity", -> - @EditorController.moveEntity - .calledWith(@project_id, @entity_id, @folder_id, @entity_type, @userId) - .should.equal true - - it "should send back a success response", -> - @res.sendStatus.calledWith(204).should.equal true - - describe "deleteEntity", -> - beforeEach -> - @req.params = - Project_id: @project_id - entity_id: @entity_id = "entity-id-123" - entity_type: @entity_type = "entity-type" - @EditorController.deleteEntity = sinon.stub().callsArg(5) - @EditorHttpController.deleteEntity @req, @res - - it "should call EditorController.deleteEntity", -> - @EditorController.deleteEntity - .calledWith(@project_id, @entity_id, @entity_type, "editor", @userId) - .should.equal true - - it "should send back a success response", -> - @res.sendStatus.calledWith(204).should.equal true diff --git a/services/web/test/unit/coffee/Editor/EditorRealTimeControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorRealTimeControllerTests.coffee deleted file mode 100644 index 64c1329131..0000000000 --- a/services/web/test/unit/coffee/Editor/EditorRealTimeControllerTests.coffee +++ /dev/null @@ -1,45 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor/EditorRealTimeController' - -describe "EditorRealTimeController", -> - beforeEach -> - @rclient = - publish: sinon.stub() - @EditorRealTimeController = SandboxedModule.require modulePath, requires: - "../../infrastructure/RedisWrapper": - client: () => @rclient - "../../infrastructure/Server" : io: @io = {} - "settings-sharelatex":{redis:{}} - "crypto": @crypto = { randomBytes: sinon.stub().withArgs(4).returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) } - "os": @os = {hostname: sinon.stub().returns("somehost")} - - @room_id = "room-id" - @message = "message-to-editor" - @payload = ["argument one", 42] - - describe "emitToRoom", -> - beforeEach -> - @message_id = "web:somehost:01020304-0" - @EditorRealTimeController.emitToRoom(@room_id, @message, @payload...) - - it "should publish the message to redis", -> - @rclient.publish - .calledWith("editor-events", JSON.stringify( - room_id: @room_id, - message: @message - payload: @payload - _id: @message_id - )) - .should.equal true - - describe "emitToAll", -> - beforeEach -> - @EditorRealTimeController.emitToRoom = sinon.stub() - @EditorRealTimeController.emitToAll @message, @payload... - - it "should emit to the room 'all'", -> - @EditorRealTimeController.emitToRoom - .calledWith("all", @message, @payload...) - .should.equal true diff --git a/services/web/test/unit/coffee/Email/EmailBuilderTests.coffee b/services/web/test/unit/coffee/Email/EmailBuilderTests.coffee deleted file mode 100644 index 84682b06af..0000000000 --- a/services/web/test/unit/coffee/Email/EmailBuilderTests.coffee +++ /dev/null @@ -1,74 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Email/EmailBuilder" -expect = require("chai").expect -_ = require('underscore') -_.templateSettings = - interpolate: /\{\{(.+?)\}\}/g - -describe "EmailBuilder", -> - - beforeEach -> - - @settings = - appName: "testApp" - brandPrefix: '' - @EmailBuilder = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - - describe "projectInvite", -> - beforeEach -> - @opts = - to:"bob@bob.com" - first_name:"bob" - owner: - email:"sally@hally.com" - inviteUrl: "http://example.com/invite" - project: - url:"http://www.project.com" - name:"standard project" - - describe "when sending a normal email", -> - beforeEach -> - @email = @EmailBuilder.buildEmail("projectInvite", @opts) - - it 'should have html and text properties', -> - expect(@email.html?).to.equal true - expect(@email.text?).to.equal true - - it "should not have undefined in it", -> - @email.html.indexOf("undefined").should.equal -1 - @email.subject.indexOf("undefined").should.equal -1 - - describe "when someone is up to no good", -> - beforeEach -> - @opts.project.name = "" - @email = @EmailBuilder.buildEmail("projectInvite", @opts) - - it 'should not contain unescaped html in the html part', -> - expect(@email.html).to.contain "New Project" - - it "should not have undefined in it", -> - @email.html.indexOf("undefined").should.equal -1 - @email.subject.indexOf("undefined").should.equal -1 - - describe "SpamSafe", -> - beforeEach -> - @opts = - to:"bob@joe.com" - first_name:"bob" - owner: - email:"sally@hally.com" - inviteUrl: "http://example.com/invite" - project: - url:"http://www.project.com" - name:"come buy my product at http://notascam.com" - @email = @EmailBuilder.buildEmail("projectInvite", @opts) - - it "should replace spammy project name", -> - @email.html.indexOf("a new project").should.not.equal -1 - @email.subject.indexOf("New Project").should.not.equal -1 diff --git a/services/web/test/unit/coffee/Email/EmailHandlerTests.coffee b/services/web/test/unit/coffee/Email/EmailHandlerTests.coffee deleted file mode 100644 index 90686f15b8..0000000000 --- a/services/web/test/unit/coffee/Email/EmailHandlerTests.coffee +++ /dev/null @@ -1,92 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Email/EmailHandler" -expect = require("chai").expect - -describe "EmailHandler", -> - - beforeEach -> - - @settings = - email:{} - @EmailBuilder = - buildEmail:sinon.stub() - @EmailSender = - sendEmail:sinon.stub() - @EmailHandler = SandboxedModule.require modulePath, requires: - "./EmailBuilder":@EmailBuilder - "./EmailSender":@EmailSender - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - - @html = "hello" - - describe "send email", -> - - it "should use the correct options", (done)-> - @EmailBuilder.buildEmail.returns({html:@html}) - @EmailSender.sendEmail.callsArgWith(1) - - opts = - to: "bob@bob.com" - @EmailHandler.sendEmail "welcome", opts, => - args = @EmailSender.sendEmail.args[0][0] - args.html.should.equal @html - done() - - it "should return the erroor", (done)-> - @EmailBuilder.buildEmail.returns({html:@html}) - @EmailSender.sendEmail.callsArgWith(1, "error") - - opts = - to: "bob@bob.com" - subject:"hello bob" - @EmailHandler.sendEmail "welcome", opts, (err)=> - err.should.equal "error" - done() - - it "should not send an email if lifecycle is not enabled", (done)-> - @settings.email.lifecycle = false - @EmailBuilder.buildEmail.returns({type:"lifecycle"}) - @EmailHandler.sendEmail "welcome", {}, => - @EmailSender.sendEmail.called.should.equal false - done() - - it "should send an email if lifecycle is not enabled but the type is notification", (done)-> - @settings.email.lifecycle = false - @EmailBuilder.buildEmail.returns({type:"notification"}) - @EmailSender.sendEmail.callsArgWith(1) - opts = - to: "bob@bob.com" - @EmailHandler.sendEmail "welcome", opts, => - @EmailSender.sendEmail.called.should.equal true - done() - - it "should send lifecycle email if it is enabled", (done)-> - @settings.email.lifecycle = true - @EmailBuilder.buildEmail.returns({type:"lifecycle"}) - @EmailSender.sendEmail.callsArgWith(1) - opts = - to: "bob@bob.com" - @EmailHandler.sendEmail "welcome", opts, => - @EmailSender.sendEmail.called.should.equal true - done() - - describe 'with plain-text email content', () -> - - beforeEach -> - @text = "hello there" - - it 'should pass along the text field', (done) -> - @EmailBuilder.buildEmail.returns({html: @html, text: @text}) - @EmailSender.sendEmail.callsArgWith(1) - opts = - to: "bob@bob.com" - @EmailHandler.sendEmail "welcome", opts, => - args = @EmailSender.sendEmail.args[0][0] - args.html.should.equal @html - args.text.should.equal @text - done() diff --git a/services/web/test/unit/coffee/Email/EmailSenderTests.coffee b/services/web/test/unit/coffee/Email/EmailSenderTests.coffee deleted file mode 100644 index 958cf208b3..0000000000 --- a/services/web/test/unit/coffee/Email/EmailSenderTests.coffee +++ /dev/null @@ -1,135 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Email/EmailSender.js" -expect = require("chai").expect - -describe "EmailSender", -> - - beforeEach -> - - @RateLimiter = - addCount:sinon.stub() - - @settings = - email: - transport: "ses" - parameters: - AWSAccessKeyID: "key" - AWSSecretKey: "secret" - fromAddress: "bob@bob.com" - replyToAddress: "sally@gmail.com" - - @sesClient = - sendMail: sinon.stub() - - @ses = - createTransport: => @sesClient - - - @sender = SandboxedModule.require modulePath, requires: - 'nodemailer': @ses - "nodemailer-mandrill-transport":{} - "nodemailer-sendgrid-transport":{} - "settings-sharelatex":@settings - '../../infrastructure/RateLimiter':@RateLimiter - "logger-sharelatex": - log:-> - warn:-> - err:-> - "metrics-sharelatex": inc:-> - - - - @opts = - to: "bob@bob.com" - subject: "new email" - html: "" - - describe "sendEmail", -> - - it "should set the properties on the email to send", (done)-> - @sesClient.sendMail.callsArgWith(1) - - @sender.sendEmail @opts, (err) => - expect(err).to.not.exist - args = @sesClient.sendMail.args[0][0] - args.html.should.equal @opts.html - args.to.should.equal @opts.to - args.subject.should.equal @opts.subject - done() - - it "should return a non-specific error", (done)-> - @sesClient.sendMail.callsArgWith(1, "error") - @sender.sendEmail {}, (err)=> - err.should.exist - err.toString().should.equal 'Error: Cannot send email' - done() - - - it "should use the from address from settings", (done)-> - @sesClient.sendMail.callsArgWith(1) - - @sender.sendEmail @opts, => - args = @sesClient.sendMail.args[0][0] - args.from.should.equal @settings.email.fromAddress - done() - - it "should use the reply to address from settings", (done)-> - @sesClient.sendMail.callsArgWith(1) - - @sender.sendEmail @opts, => - args = @sesClient.sendMail.args[0][0] - args.replyTo.should.equal @settings.email.replyToAddress - done() - - - it "should use the reply to address in options as an override", (done)-> - @sesClient.sendMail.callsArgWith(1) - - @opts.replyTo = "someone@else.com" - @sender.sendEmail @opts, => - args = @sesClient.sendMail.args[0][0] - args.replyTo.should.equal @opts.replyTo - done() - - - it "should not send an email when the rate limiter says no", (done)-> - @opts.sendingUser_id = "12321312321" - @RateLimiter.addCount.callsArgWith(1, null, false) - @sender.sendEmail @opts, => - @sesClient.sendMail.called.should.equal false - done() - - it "should send the email when the rate limtier says continue", (done)-> - @sesClient.sendMail.callsArgWith(1) - @opts.sendingUser_id = "12321312321" - @RateLimiter.addCount.callsArgWith(1, null, true) - @sender.sendEmail @opts, => - @sesClient.sendMail.called.should.equal true - done() - - it "should not check the rate limiter when there is no sendingUser_id", (done)-> - @sesClient.sendMail.callsArgWith(1) - @sender.sendEmail @opts, => - @sesClient.sendMail.called.should.equal true - @RateLimiter.addCount.called.should.equal false - done() - - describe 'with plain-text email content', () -> - - beforeEach -> - @opts.text = "hello there" - - it "should set the text property on the email to send", (done)-> - @sesClient.sendMail.callsArgWith(1) - - @sender.sendEmail @opts, => - args = @sesClient.sendMail.args[0][0] - args.html.should.equal @opts.html - args.text.should.equal @opts.text - args.to.should.equal @opts.to - args.subject.should.equal @opts.subject - done() diff --git a/services/web/test/unit/coffee/Email/SpamSafeTests.coffee b/services/web/test/unit/coffee/Email/SpamSafeTests.coffee deleted file mode 100644 index d935dcd0c0..0000000000 --- a/services/web/test/unit/coffee/Email/SpamSafeTests.coffee +++ /dev/null @@ -1,29 +0,0 @@ -path = require('path') -modulePath = path.join __dirname, "../../../../app/js/Features/Email/SpamSafe" -SpamSafe = require(modulePath) -expect = require("chai").expect - -describe "SpamSafe", -> - - it 'should reject spammy names', -> - expect(SpamSafe.isSafeUserName("Charline Wałęsa")).to.equal true - expect(SpamSafe.isSafeUserName("hey come buy this product im selling it's really good for you and it'll make your latex 10x guaranteed")).to.equal false - expect(SpamSafe.isSafeUserName("隆太郎 宮本")).to.equal true - expect(SpamSafe.isSafeUserName("Visit haxx0red.com")).to.equal false - expect(SpamSafe.isSafeUserName('加美汝VX:hihi661,金沙2001005com the first deposit will be _100%_')).to.equal false - expect(SpamSafe.isSafeProjectName('Neural Networks: good for your health and will solve all your problems')).to.equal false - expect(SpamSafe.isSafeProjectName("An analysis of the questions of the universe!")).to.equal true - expect(SpamSafe.isSafeProjectName("A'p'o's't'r'o'p'h'e gallore")).to.equal true - expect(SpamSafe.isSafeProjectName('come buy this => http://www.dopeproduct.com/search/?q=user123')).to.equal false - expect(SpamSafe.isSafeEmail("realistic-email+1@domain.sub-hyphen.com")).to.equal true - expect(SpamSafe.isSafeEmail("notquiteRight\@evil$.com")).to.equal false - - expect(SpamSafe.safeUserName("Tammy Weinstįen", "A User")).to.equal "Tammy Weinstįen" - expect(SpamSafe.safeUserName("haxx0red.com", "A User")).to.equal "A User" - expect(SpamSafe.safeUserName("What$ Upp", "A User")).to.equal "A User" - expect(SpamSafe.safeProjectName("Math-ematics!", "A Project")).to.equal "Math-ematics!" - expect(SpamSafe.safeProjectName("A Very long title for a very long book that will never be read" + "a".repeat(250), "A Project")).to.equal "A Project" - expect(SpamSafe.safeEmail("safe-ëmail@domain.com", "A collaborator")).to.equal "safe-ëmail@domain.com" - expect(SpamSafe.safeEmail("Բարեւ@another.domain", "A collaborator")).to.equal "Բարեւ@another.domain" - expect(SpamSafe.safeEmail("me+" + "a".repeat(40) + "@googoole.con", "A collaborator")).to.equal "A collaborator" - expect(SpamSafe.safeEmail("sendME$$$@iAmAprince.com", "A collaborator")).to.equal "A collaborator" diff --git a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee deleted file mode 100644 index 60a68b4e52..0000000000 --- a/services/web/test/unit/coffee/Exports/ExportsControllerTests.coffee +++ /dev/null @@ -1,126 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -chai = require('chai') -expect = chai.expect -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Exports/ExportsController.js' - - -describe 'ExportsController', -> - project_id = "123njdskj9jlk" - user_id = "123nd3ijdks" - brand_variation_id = 22 - firstName = 'first' - lastName = 'last' - title = "title" - description = "description" - author = "author" - license = "other" - show_source = true - - beforeEach -> - @handler = - getUserNotifications: sinon.stub().callsArgWith(1) - @req = - params: - project_id: project_id - brand_variation_id: brand_variation_id - body: - firstName: firstName - lastName: lastName - session: - user: - _id:user_id - i18n: - translate:-> - @res = - json: sinon.stub() - status: sinon.stub() - @res.status.returns(@res) - @next = sinon.stub() - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@req.session.user._id) - @controller = SandboxedModule.require modulePath, requires: - "./ExportsHandler":@handler - 'logger-sharelatex': - log:-> - err:-> - '../Authentication/AuthenticationController': @AuthenticationController - - describe "without gallery fields",-> - it 'should ask the handler to perform the export', (done) -> - @handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897}) - expected = - project_id: project_id - user_id: user_id - brand_variation_id: brand_variation_id - first_name: firstName - last_name: lastName - @controller.exportProject @req, json:(body) => - expect(@handler.exportProject.args[0][0]).to.deep.equal expected - expect(body).to.deep.equal {export_v1_id: 897} - done() - - describe "with gallery fields",-> - beforeEach -> - @req.body.title = title - @req.body.description = description - @req.body.author = author - @req.body.license = license - @req.body.showSource = true - - it 'should ask the handler to perform the export', (done) -> - @handler.exportProject = sinon.stub().yields(null, {iAmAnExport: true, v1_id: 897}) - expected = - project_id: project_id - user_id: user_id - brand_variation_id: brand_variation_id - first_name: firstName - last_name: lastName - title: title - description: description - author: author - license: license - show_source: show_source - @controller.exportProject @req, json:(body) => - expect(@handler.exportProject.args[0][0]).to.deep.equal expected - expect(body).to.deep.equal {export_v1_id: 897} - done() - - describe "with an error return from v1 to forward to the publish modal",-> - it 'should forward the response onward', (done) -> - @error_json = { status: 422, message: 'nope' } - @handler.exportProject = sinon.stub().yields({forwardResponse: @error_json}) - @controller.exportProject @req, @res, @next - expect(@res.json.args[0][0]).to.deep.equal @error_json - expect(@res.status.args[0][0]).to.equal @error_json.status - done() - - it 'should ask the handler to return the status of an export', (done) -> - @handler.fetchExport = sinon.stub().yields( - null, - "{ - \"id\":897, - \"status_summary\":\"completed\", - \"status_detail\":\"all done\", - \"partner_submission_id\":\"abc123\", - \"v2_user_email\":\"la@tex.com\", - \"v2_user_first_name\":\"Arthur\", - \"v2_user_last_name\":\"Author\", - \"title\":\"my project\", - \"token\":\"token\" - }") - - @req.params = {project_id: project_id, export_id: 897} - @controller.exportStatus @req, json:(body) => - expect(body).to.deep.equal {export_json: { - status_summary: 'completed', - status_detail: "all done", - partner_submission_id: "abc123", - v2_user_email: "la@tex.com", - v2_user_first_name: "Arthur", - v2_user_last_name: "Author", - title: "my project", - token: "token" - }} - done() diff --git a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee deleted file mode 100644 index 2db310cafb..0000000000 --- a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee +++ /dev/null @@ -1,455 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = '../../../../app/js/Features/Exports/ExportsHandler.js' -SandboxedModule = require('sandboxed-module') - -describe 'ExportsHandler', -> - - beforeEach -> - @stubRequest = {} - @request = defaults: => return @stubRequest - @ExportsHandler = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': - log: -> - err: -> - '../Project/ProjectGetter': @ProjectGetter = {} - '../Project/ProjectHistoryHandler': @ProjectHistoryHandler = {} - '../Project/ProjectLocator': @ProjectLocator = {} - '../Project/ProjectRootDocManager': @ProjectRootDocManager = {} - '../User/UserGetter': @UserGetter = {} - 'settings-sharelatex': @settings = {} - 'request': @request - @project_id = "project-id-123" - @project_history_id = 987 - @user_id = "user-id-456" - @brand_variation_id = 789 - @title = "title" - @description = "description" - @author = "author" - @license = "other" - @show_source = true - @export_params = { - project_id: @project_id, - brand_variation_id: @brand_variation_id, - user_id: @user_id - title: @title - description: @description - author: @author - license: @license - show_source: @show_source - } - @callback = sinon.stub() - - describe 'exportProject', -> - beforeEach -> - @export_data = {iAmAnExport: true} - @response_body = {iAmAResponseBody: true} - @ExportsHandler._buildExport = sinon.stub().yields(null, @export_data) - @ExportsHandler._requestExport = sinon.stub().yields(null, @response_body) - - describe "when all goes well", -> - beforeEach (done) -> - @ExportsHandler.exportProject @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should build the export", -> - @ExportsHandler._buildExport - .calledWith(@export_params) - .should.equal true - - it "should request the export", -> - @ExportsHandler._requestExport - .calledWith(@export_data) - .should.equal true - - it "should return the export", -> - @callback - .calledWith(null, @export_data) - .should.equal true - - describe "when request can't be built", -> - beforeEach (done) -> - @ExportsHandler._buildExport = sinon.stub().yields(new Error("cannot export project without root doc")) - @ExportsHandler.exportProject @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should return an error", -> - (@callback.args[0][0] instanceof Error) - .should.equal true - - - describe "when export request returns an error to forward to the user", -> - beforeEach (done) -> - @error_json = { status: 422, message: 'nope' } - @ExportsHandler._requestExport = sinon.stub().yields(null, forwardResponse: @error_json) - @ExportsHandler.exportProject @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should return success and the response to forward", -> - (@callback.args[0][0] instanceof Error) - .should.equal false - @callback.calledWith(null, {forwardResponse: @error_json}) - - describe '_buildExport', -> - beforeEach (done) -> - @project = - id: @project_id - rootDoc_id: 'doc1_id' - compiler: 'pdflatex' - imageName: 'mock-image-name' - overleaf: - id: @project_history_id # for projects imported from v1 - history: - id: @project_history_id - @user = - id: @user_id - first_name: 'Arthur' - last_name: 'Author' - email: 'arthur.author@arthurauthoring.org' - overleaf: - id: 876 - @rootDocPath = 'main.tex' - @historyVersion = 777 - @ProjectGetter.getProject = sinon.stub().yields(null, @project) - @ProjectHistoryHandler.ensureHistoryExistsForProject = sinon.stub().yields(null) - @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'main.tex'}]) - @ProjectRootDocManager.ensureRootDocumentIsValid = sinon.stub().callsArgWith(1, null) - @UserGetter.getUser = sinon.stub().yields(null, @user) - @ExportsHandler._requestVersion = sinon.stub().yields(null, @historyVersion) - done() - - describe "when all goes well", -> - beforeEach (done) -> - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should ensure the project has history", -> - @ProjectHistoryHandler.ensureHistoryExistsForProject.called - .should.equal true - - it "should request the project history version", -> - @ExportsHandler._requestVersion.called - .should.equal true - - it "should return export data", -> - expected_export_data = - project: - id: @project_id - rootDocPath: @rootDocPath - historyId: @project_history_id - historyVersion: @historyVersion - v1ProjectId: @project_history_id - metadata: - compiler: 'pdflatex' - imageName: 'mock-image-name' - title: @title - description: @description - author: @author - license: @license - showSource: @show_source - user: - id: @user_id - firstName: @user.first_name - lastName: @user.last_name - email: @user.email - orcidId: null - v1UserId: 876 - destination: - brandVariationId: @brand_variation_id - options: - callbackUrl: null - @callback.calledWith(null, expected_export_data) - .should.equal true - - describe "when we send replacement user first and last name", -> - beforeEach (done) -> - @custom_first_name = "FIRST" - @custom_last_name = "LAST" - @export_params.first_name = @custom_first_name - @export_params.last_name = @custom_last_name - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should send the data from the user input", -> - expected_export_data = - project: - id: @project_id - rootDocPath: @rootDocPath - historyId: @project_history_id - historyVersion: @historyVersion - v1ProjectId: @project_history_id - metadata: - compiler: 'pdflatex' - imageName: 'mock-image-name' - title: @title - description: @description - author: @author - license: @license - showSource: @show_source - user: - id: @user_id - firstName: @custom_first_name - lastName: @custom_last_name - email: @user.email - orcidId: null - v1UserId: 876 - destination: - brandVariationId: @brand_variation_id - options: - callbackUrl: null - @callback.calledWith(null, expected_export_data) - .should.equal true - - describe "when project is not found", -> - beforeEach (done) -> - @ProjectGetter.getProject = sinon.stub().yields(new Error("project not found")) - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should return an error", -> - (@callback.args[0][0] instanceof Error) - .should.equal true - - describe "when project has no root doc", -> - describe "when a root doc can be set automatically", -> - beforeEach (done) -> - @project.rootDoc_id = null - @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'other.tex'}]) - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should set a root doc", -> - @ProjectRootDocManager.ensureRootDocumentIsValid.called - .should.equal true - - it "should return export data", -> - expected_export_data = - project: - id: @project_id - rootDocPath: 'other.tex' - historyId: @project_history_id - historyVersion: @historyVersion - v1ProjectId: @project_history_id - metadata: - compiler: 'pdflatex' - imageName: 'mock-image-name' - title: @title - description: @description - author: @author - license: @license - showSource: @show_source - user: - id: @user_id - firstName: @user.first_name - lastName: @user.last_name - email: @user.email - orcidId: null - v1UserId: 876 - destination: - brandVariationId: @brand_variation_id - options: - callbackUrl: null - @callback.calledWith(null, expected_export_data) - .should.equal true - - describe "when project has an invalid root doc", -> - describe "when a new root doc can be set automatically", -> - beforeEach (done) -> - @fakeDoc_id = '1a2b3c4d5e6f' - @project.rootDoc_id = @fakeDoc_id - @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'other.tex'}]) - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should set a valid root doc", -> - @ProjectRootDocManager.ensureRootDocumentIsValid.called - .should.equal true - - it "should return export data", -> - expected_export_data = - project: - id: @project_id - rootDocPath: 'other.tex' - historyId: @project_history_id - historyVersion: @historyVersion - v1ProjectId: @project_history_id - metadata: - compiler: 'pdflatex' - imageName: 'mock-image-name' - title: @title - description: @description - author: @author - license: @license - showSource: @show_source - user: - id: @user_id - firstName: @user.first_name - lastName: @user.last_name - email: @user.email - orcidId: null - v1UserId: 876 - destination: - brandVariationId: @brand_variation_id - options: - callbackUrl: null - @callback.calledWith(null, expected_export_data) - .should.equal true - - describe "when no root doc can be identified", -> - beforeEach (done) -> - @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null]) - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should return an error", -> - (@callback.args[0][0] instanceof Error) - .should.equal true - - describe "when user is not found", -> - beforeEach (done) -> - @UserGetter.getUser = sinon.stub().yields(new Error("user not found")) - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should return an error", -> - (@callback.args[0][0] instanceof Error) - .should.equal true - - describe "when project history request fails", -> - beforeEach (done) -> - @ExportsHandler._requestVersion = sinon.stub().yields(new Error("project history call failed")) - @ExportsHandler._buildExport @export_params, (error, export_data) => - @callback(error, export_data) - done() - - it "should return an error", -> - (@callback.args[0][0] instanceof Error) - .should.equal true - - describe '_requestExport', -> - beforeEach (done) -> - @settings.apis = - v1: - url: 'http://localhost:5000' - user: 'overleaf' - pass: 'pass' - @export_data = {iAmAnExport: true} - @export_id = 4096 - @stubPost = sinon.stub().yields(null, {statusCode: 200}, { exportId: @export_id }) - done() - - describe "when all goes well", -> - beforeEach (done) -> - @stubRequest.post = @stubPost - @ExportsHandler._requestExport @export_data, (error, export_v1_id) => - @callback(error, export_v1_id) - done() - - it 'should issue the request', -> - expect(@stubPost.getCall(0).args[0]).to.deep.equal - url: @settings.apis.v1.url + '/api/v1/sharelatex/exports' - auth: - user: @settings.apis.v1.user - pass: @settings.apis.v1.pass - json: @export_data - - it 'should return the v1 export id', -> - @callback.calledWith(null, @export_id) - .should.equal true - - describe "when the request fails", -> - beforeEach (done) -> - @stubRequest.post = sinon.stub().yields(new Error("export request failed")) - @ExportsHandler._requestExport @export_data, (error, export_v1_id) => - @callback(error, export_v1_id) - done() - - it "should return an error", -> - (@callback.args[0][0] instanceof Error) - .should.equal true - - describe "when the request returns an error response to forward", -> - beforeEach (done) -> - @error_code = 422 - @error_json = { status: @error_code, message: 'nope' } - @stubRequest.post = sinon.stub().yields(null, {statusCode: @error_code}, @error_json) - @ExportsHandler._requestExport @export_data, (error, export_v1_id) => - @callback(error, export_v1_id) - done() - - it "should return success and the response to forward", -> - (@callback.args[0][0] instanceof Error) - .should.equal false - @callback.calledWith(null, {forwardResponse: @error_json}) - - describe 'fetchExport', -> - beforeEach (done) -> - @settings.apis = - v1: - url: 'http://localhost:5000' - user: 'overleaf' - pass: 'pass' - @export_id = 897 - @body = "{\"id\":897, \"status_summary\":\"completed\"}" - @stubGet = sinon.stub().yields(null, {statusCode: 200}, { body: @body }) - done() - - describe "when all goes well", -> - beforeEach (done) -> - @stubRequest.get = @stubGet - @ExportsHandler.fetchExport @export_id, (error, body) => - @callback(error, body) - done() - - it 'should issue the request', -> - expect(@stubGet.getCall(0).args[0]).to.deep.equal - url: @settings.apis.v1.url + '/api/v1/sharelatex/exports/' + @export_id - auth: - user: @settings.apis.v1.user - pass: @settings.apis.v1.pass - - it 'should return the v1 export id', -> - @callback.calledWith(null, { body: @body }) - .should.equal true - - describe 'fetchDownload', -> - beforeEach (done) -> - @settings.apis = - v1: - url: 'http://localhost:5000' - user: 'overleaf' - pass: 'pass' - @export_id = 897 - @body = "https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee" - @stubGet = sinon.stub().yields(null, {statusCode: 200}, { body: @body }) - done() - - describe "when all goes well", -> - beforeEach (done) -> - @stubRequest.get = @stubGet - @ExportsHandler.fetchDownload @export_id, 'zip', (error, body) => - @callback(error, body) - done() - - it 'should issue the request', -> - expect(@stubGet.getCall(0).args[0]).to.deep.equal - url: @settings.apis.v1.url + '/api/v1/sharelatex/exports/' + @export_id + '/zip_url' - auth: - user: @settings.apis.v1.user - pass: @settings.apis.v1.pass - - it 'should return the v1 export id', -> - @callback.calledWith(null, { body: @body }) - .should.equal true diff --git a/services/web/test/unit/coffee/FileStore/FileStoreControllerTests.coffee b/services/web/test/unit/coffee/FileStore/FileStoreControllerTests.coffee deleted file mode 100644 index 7c59be92f0..0000000000 --- a/services/web/test/unit/coffee/FileStore/FileStoreControllerTests.coffee +++ /dev/null @@ -1,136 +0,0 @@ -assert = require("chai").assert -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/FileStore/FileStoreController.js" -SandboxedModule = require('sandboxed-module') - -describe "FileStoreController", -> - - beforeEach -> - @FileStoreHandler = - getFileStream: sinon.stub() - @ProjectLocator = - findElement: sinon.stub() - @controller = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()} - "../Project/ProjectLocator": @ProjectLocator - "./FileStoreHandler": @FileStoreHandler - @stream = {} - @project_id = "2k3j1lk3j21lk3j" - @file_id = "12321kklj1lk3jk12" - @req = - params: - Project_id: @project_id - File_id: @file_id - query: "query string here" - get: (key) -> undefined - @res = - setHeader: sinon.stub() - setContentDisposition: sinon.stub() - @file = - name: "myfile.png" - - describe "getFile", -> - - beforeEach -> - @FileStoreHandler.getFileStream.callsArgWith(3, null, @stream) - @ProjectLocator.findElement.callsArgWith(1, null, @file) - - it "should call the file store handler with the project_id file_id and any query string", (done)-> - @stream.pipe = (des)=> - @FileStoreHandler.getFileStream.calledWith(@req.params.Project_id, @req.params.File_id, @req.query).should.equal true - done() - @controller.getFile @req, @res - - it "should pipe to res", (done)-> - @stream.pipe = (des)=> - des.should.equal @res - done() - @controller.getFile @req, @res - - it "should get the file from the db", (done)-> - @stream.pipe = (des)=> - opts = - project_id: @project_id - element_id: @file_id - type: "file" - @ProjectLocator.findElement.calledWith(opts).should.equal true - done() - @controller.getFile @req, @res - - it "should set the Content-Disposition header", (done)-> - @stream.pipe = (des)=> - @res.setContentDisposition.calledWith( - "attachment", {filename: @file.name} - ).should.equal true - done() - @controller.getFile @req, @res - - # Test behaviour around handling html files - ['.html', '.htm', '.xhtml'].forEach (extension) -> - describe "with a '#{extension}' file extension", -> - - beforeEach -> - @file.name = "bad#{extension}" - @req.get = (key) => - if key == 'User-Agent' - return 'A generic browser' - - describe "from a non-ios browser", -> - - it "should not set Content-Type", (done) -> - @stream.pipe = (des) => - @res.setHeader.calledWith("Content-Type", "text/plain").should.equal false - done() - @controller.getFile @req, @res - - describe "from an iPhone", -> - - beforeEach -> - @req.get = (key) => - if key == 'User-Agent' - return "An iPhone browser" - - it "should set Content-Type to 'text/plain'", (done) -> - @stream.pipe = (des) => - @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true - done() - @controller.getFile @req, @res - - describe "from an iPad", -> - - beforeEach -> - @req.get = (key) => - if key == 'User-Agent' - return "An iPad browser" - - it "should set Content-Type to 'text/plain'", (done) -> - @stream.pipe = (des) => - @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true - done() - @controller.getFile @req, @res - - # None of these should trigger the iOS/html logic - ['x.html-is-rad', 'html.pdf', '.html-is-good-for-hidden-files', 'somefile'].forEach (filename) -> - describe "with filename as '#{filename}'", -> - - beforeEach -> - @user_agent = 'A generic browser' - @file.name = filename - @req.get = (key) => - if key == 'User-Agent' - @user_agent - - ['iPhone', 'iPad', 'Firefox', 'Chrome'].forEach (browser) -> - describe "downloaded from #{browser}", -> - beforeEach -> - @user_agent = "Some #{browser} thing" - - it 'Should not set the Content-type', (done) -> - @stream.pipe = (des) => - @res.setHeader.calledWith("Content-Type", "text/plain").should.equal false - done() - @controller.getFile @req, @res diff --git a/services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee b/services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee deleted file mode 100644 index 16389e8687..0000000000 --- a/services/web/test/unit/coffee/FileStore/FileStoreHandlerTests.coffee +++ /dev/null @@ -1,260 +0,0 @@ -assert = require("chai").assert -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/FileStore/FileStoreHandler.js" -SandboxedModule = require('sandboxed-module') - -describe "FileStoreHandler", -> - beforeEach -> - @fs = - createReadStream : sinon.stub() - lstat: sinon.stub().callsArgWith(1, null, { - isFile:=> true - isDirectory:-> return false - }) - @writeStream = - my:"writeStream" - on: (type, cb)-> - if type == "response" - cb({statusCode: 200}) - @readStream = {my:"readStream", on: sinon.stub()} - @request = sinon.stub() - @settings = apis:{filestore:{url:"http//filestore.sharelatex.test"}} - @hashValue = "0123456789" - @FileModel = class File - constructor:(options)-> - {@name,@hash} = options - @_id = "file_id_here" - @rev = 0 - if options.linkedFileData? - @linkedFileData = options.linkedFileData - @handler = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "request":@request - "logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()} - "./FileHashManager" : @FileHashManager = { computeHash: sinon.stub().callsArgWith(1, null, @hashValue)} - # FIXME: need to stub File object here - "../../models/File" : File: @FileModel - "fs" : @fs - @file_args = {name: "upload-filename"} - @file_id = "file_id_here" - @project_id = "1312312312" - @fsPath = "uploads/myfile.eps" - @handler._buildUrl = sinon.stub().returns("http://filestore.stubbedBuilder.com") - - describe "uploadFileFromDisk", -> - beforeEach -> - @request.returns(@writeStream) - - it "should create read stream", (done)-> - @fs.createReadStream.returns - pipe:-> - on: (type, cb)-> - if type == "open" - cb() - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, => - @fs.createReadStream.calledWith(@fsPath).should.equal true - done() - - it "should pipe the read stream to request", (done)-> - @request.returns(@writeStream) - @fs.createReadStream.returns - on: (type, cb)-> - if type == "open" - cb() - pipe:(o)=> - @writeStream.should.equal o - done() - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, => - - it "should pass the correct options to request", (done)-> - @fs.createReadStream.returns - pipe:-> - on: (type, cb)-> - if type == "open" - cb() - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, => - @request.args[0][0].method.should.equal "post" - @request.args[0][0].uri.should.equal @handler._buildUrl() - done() - - it "builds the correct url", (done)-> - @fs.createReadStream.returns - pipe:-> - on: (type, cb)-> - if type == "open" - cb() - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, => - @handler._buildUrl.calledWith(@project_id, @file_id).should.equal true - done() - - it 'should callback with the url and fileRef', (done) -> - @fs.createReadStream.returns - pipe:-> - on: (type, cb)-> - if type == "open" - cb() - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, (err, url, fileRef) => - expect(err).to.not.exist - expect(url).to.equal(@handler._buildUrl()) - expect(fileRef._id).to.equal(@file_id) - expect(fileRef.hash).to.equal(@hashValue) - done() - - describe "symlink", -> - beforeEach -> - @fs.lstat = sinon.stub().callsArgWith(1, null, { - isFile:=> false - isDirectory:-> return false - }) - - it "should not read file if it is symlink", (done)-> - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, => - @fs.createReadStream.called.should.equal false - done() - - describe "symlink", -> - it "should not read file stat returns nothing", (done)-> - @fs.lstat = sinon.stub().callsArgWith(1, null, null) - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, => - @fs.createReadStream.called.should.equal false - done() - - describe "when upload fails", -> - beforeEach -> - @writeStream.on = (type, cb) -> - if type == "response" - cb({statusCode: 500}) - - it 'should callback with an error', (done) -> - @fs.createReadStream.callCount = 0 - @fs.createReadStream.returns - pipe:-> - on: (type, cb)-> - if type == "open" - cb() - @handler.uploadFileFromDisk @project_id, @file_args, @fsPath, (err) => - expect(err).to.exist - expect(err).to.be.instanceof Error - expect(@fs.createReadStream.callCount).to.equal @handler.RETRY_ATTEMPTS - done() - - describe "deleteFile", -> - - it "should send a delete request to filestore api", (done)-> - @request.callsArgWith(1, null) - @handler.deleteFile @project_id, @file_id, (err)=> - assert.equal err, undefined - @request.args[0][0].method.should.equal "delete" - @request.args[0][0].uri.should.equal @handler._buildUrl() - done() - - it "should return the error if there is one", (done)-> - error = "my error" - @request.callsArgWith(1, error) - @handler.deleteFile @project_id, @file_id, (err)=> - assert.equal err, error - done() - - it "builds the correct url", (done)-> - @request.callsArgWith(1, null) - @handler.deleteFile @project_id, @file_id, (err)=> - @handler._buildUrl.calledWith(@project_id, @file_id).should.equal true - done() - - describe "getFileStream", -> - beforeEach -> - @query = {} - @request.returns(@readStream) - - it "should get the stream with the correct params", (done)-> - @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> - @request.args[0][0].method.should.equal "get" - @request.args[0][0].uri.should.equal @handler._buildUrl() - done() - - it "should get stream from request", (done)-> - @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> - stream.should.equal @readStream - done() - - it "builds the correct url", (done)-> - @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> - @handler._buildUrl.calledWith(@project_id, @file_id).should.equal true - done() - - it "should add an error handler", (done) -> - @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> - stream.on.calledWith("error").should.equal true - done() - - describe 'when range is specified in query', -> - - beforeEach -> - @query = {'range': '0-10'} - - it 'should add a range header', (done) -> - @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> - @request.callCount.should.equal 1 - headers = @request.firstCall.args[0].headers - expect(headers).to.have.keys('range') - expect(headers['range']).to.equal 'bytes=0-10' - done() - - describe 'when range is invalid', -> - - ['0-', '-100', 'one-two', 'nonsense'].forEach (r) => - - beforeEach -> - @query = {'range': "#{r}"} - - it "should not add a range header for '#{r}'", (done) -> - @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> - @request.callCount.should.equal 1 - headers = @request.firstCall.args[0].headers - expect(headers).to.not.have.keys('range') - done() - - describe "copyFile", -> - - beforeEach -> - @newProject_id = "new project" - @newFile_id = "new file id" - - it "should post json", (done)-> - @request.callsArgWith(1, null, {statusCode: 200}) - - @handler.copyFile @project_id, @file_id, @newProject_id, @newFile_id, => - @request.args[0][0].method.should.equal "put" - @request.args[0][0].uri.should.equal @handler._buildUrl() - @request.args[0][0].json.source.project_id.should.equal @project_id - @request.args[0][0].json.source.file_id.should.equal @file_id - done() - - it "builds the correct url", (done)-> - @request.callsArgWith(1, null, {statusCode: 200}) - @handler.copyFile @project_id, @file_id, @newProject_id, @newFile_id, => - @handler._buildUrl.calledWith(@newProject_id, @newFile_id).should.equal true - done() - - it "returns the url", (done)-> - @request.callsArgWith(1, null, {statusCode: 200}) - @handler.copyFile @project_id, @file_id, @newProject_id, @newFile_id, (err, url) => - url.should.equal "http://filestore.stubbedBuilder.com" - done() - - it "should return the err", (done)-> - error = "errrror" - @request.callsArgWith(1, error) - @handler.copyFile @project_id, @file_id, @newProject_id, @newFile_id, (err)=> - err.should.equal error - done() - - it "should return an error for a non-success statusCode", (done)-> - @request.callsArgWith(1, null, {statusCode: 500}) - @handler.copyFile @project_id, @file_id, @newProject_id, @newFile_id, (err)=> - err.should.be.an('error') - err.message.should.equal 'non-ok response from filestore for copyFile: 500' - done() \ No newline at end of file diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee deleted file mode 100644 index d5777f5490..0000000000 --- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee +++ /dev/null @@ -1,246 +0,0 @@ -chai = require('chai') -chai.should() -sinon = require("sinon") - -Errors = require "../../../../app/js/Features/Errors/Errors" - -modulePath = "../../../../app/js/Features/History/HistoryController" -SandboxedModule = require('sandboxed-module') - -describe "HistoryController", -> - beforeEach -> - @callback = sinon.stub() - @user_id = "user-id-123" - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user_id) - @HistoryController = SandboxedModule.require modulePath, requires: - "request" : @request = sinon.stub() - "settings-sharelatex": @settings = {} - "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()} - "../Authentication/AuthenticationController": @AuthenticationController - "../Errors/Errors": Errors - "./HistoryManager": @HistoryManager = {} - "../Project/ProjectDetailsHandler": @ProjectDetailsHandler = {} - "../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {} - "./RestoreManager": @RestoreManager = {} - @settings.apis = - trackchanges: - enabled: false - url: "http://trackchanges.example.com" - project_history: - url: "http://project_history.example.com" - - describe "selectHistoryApi", -> - beforeEach -> - @req = { url: "/mock/url", method: "POST" } - @res = "mock-res" - @next = sinon.stub() - - describe "for a project with project history", -> - beforeEach -> - @ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{id: 42, display:true}}}) - @HistoryController.selectHistoryApi @req, @res, @next - - it "should set the flag for project history to true", -> - @req.useProjectHistory.should.equal true - - describe "for any other project ", -> - beforeEach -> - @ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {}) - @HistoryController.selectHistoryApi @req, @res, @next - - it "should not set the flag for project history to false", -> - @req.useProjectHistory.should.equal false - - - describe "proxyToHistoryApi", -> - beforeEach -> - @req = { url: "/mock/url", method: "POST" } - @res = "mock-res" - @next = sinon.stub() - @proxy = - events: {} - pipe: sinon.stub() - on: (event, handler) -> @events[event] = handler - @request.returns @proxy - - describe "for a project with the project history flag", -> - beforeEach -> - @req.useProjectHistory = true - @HistoryController.proxyToHistoryApi @req, @res, @next - - it "should get the user id", -> - @AuthenticationController.getLoggedInUserId - .calledWith(@req) - .should.equal true - - it "should call the project history api", -> - @request - .calledWith({ - url: "#{@settings.apis.project_history.url}#{@req.url}" - method: @req.method - headers: - "X-User-Id": @user_id - }) - .should.equal true - - it "should pipe the response to the client", -> - @proxy.pipe - .calledWith(@res) - .should.equal true - - describe "for a project without the project history flag", -> - beforeEach -> - @req.useProjectHistory = false - @HistoryController.proxyToHistoryApi @req, @res, @next - - it "should get the user id", -> - @AuthenticationController.getLoggedInUserId - .calledWith(@req) - .should.equal true - - it "should call the track changes api", -> - @request - .calledWith({ - url: "#{@settings.apis.trackchanges.url}#{@req.url}" - method: @req.method - headers: - "X-User-Id": @user_id - }) - .should.equal true - - it "should pipe the response to the client", -> - @proxy.pipe - .calledWith(@res) - .should.equal true - - describe "with an error", -> - beforeEach -> - @HistoryController.proxyToHistoryApi @req, @res, @next - @proxy.events["error"].call(@proxy, @error = new Error("oops")) - - it "should pass the error up the call chain", -> - @next.calledWith(@error).should.equal true - - describe "proxyToHistoryApiAndInjectUserDetails", -> - beforeEach -> - @req = { url: "/mock/url", method: "POST" } - @res = - json: sinon.stub() - @next = sinon.stub() - @request.yields(null, {statusCode: 200}, @data = "mock-data") - @HistoryManager.injectUserDetails = sinon.stub().yields(null, @data_with_users = "mock-injected-data") - - describe "for a project with the project history flag", -> - beforeEach -> - @req.useProjectHistory = true - @HistoryController.proxyToHistoryApiAndInjectUserDetails @req, @res, @next - - it "should get the user id", -> - @AuthenticationController.getLoggedInUserId - .calledWith(@req) - .should.equal true - - it "should call the project history api", -> - @request - .calledWith({ - url: "#{@settings.apis.project_history.url}#{@req.url}" - method: @req.method - json: true - headers: - "X-User-Id": @user_id - }) - .should.equal true - - it "should inject the user data", -> - @HistoryManager.injectUserDetails - .calledWith(@data) - .should.equal true - - it "should return the data with users to the client", -> - @res.json.calledWith(@data_with_users).should.equal true - - describe "for a project without the project history flag", -> - beforeEach -> - @req.useProjectHistory = false - @HistoryController.proxyToHistoryApiAndInjectUserDetails @req, @res, @next - - it "should get the user id", -> - @AuthenticationController.getLoggedInUserId - .calledWith(@req) - .should.equal true - - it "should call the track changes api", -> - @request - .calledWith({ - url: "#{@settings.apis.trackchanges.url}#{@req.url}" - method: @req.method - json: true - headers: - "X-User-Id": @user_id - }) - .should.equal true - - it "should inject the user data", -> - @HistoryManager.injectUserDetails - .calledWith(@data) - .should.equal true - - it "should return the data with users to the client", -> - @res.json.calledWith(@data_with_users).should.equal true - - describe "proxyToHistoryApiAndInjectUserDetails (with the history API failing)", -> - beforeEach -> - @req = { url: "/mock/url", method: "POST", useProjectHistory: true } - @res = { json: sinon.stub() } - @next = sinon.stub() - @request.yields(null, {statusCode: 500}, @data = "mock-data") - @HistoryManager.injectUserDetails = sinon.stub().yields(null, @data_with_users = "mock-injected-data") - @HistoryController.proxyToHistoryApiAndInjectUserDetails @req, @res, @next - - it "should not inject the user data", -> - @HistoryManager.injectUserDetails - .calledWith(@data) - .should.equal false - - it "should not return the data with users to the client", -> - @res.json.calledWith(@data_with_users).should.equal false - - describe "resyncProjectHistory", -> - describe "for a project without project-history enabled", -> - beforeEach -> - @project_id = 'mock-project-id' - @req = params: Project_id: @project_id - @res = sendStatus: sinon.stub() - @next = sinon.stub() - - @error = new Errors.ProjectHistoryDisabledError() - @ProjectEntityUpdateHandler.resyncProjectHistory = sinon.stub().yields(@error) - - @HistoryController.resyncProjectHistory @req, @res, @next - - it "response with a 404", -> - @res.sendStatus - .calledWith(404) - .should.equal true - - describe "for a project with project-history enabled", -> - beforeEach -> - @project_id = 'mock-project-id' - @req = params: Project_id: @project_id - @res = sendStatus: sinon.stub() - @next = sinon.stub() - - @ProjectEntityUpdateHandler.resyncProjectHistory = sinon.stub().yields() - - @HistoryController.resyncProjectHistory @req, @res, @next - - it "resyncs the project", -> - @ProjectEntityUpdateHandler.resyncProjectHistory - .calledWith(@project_id) - .should.equal true - - it "responds with a 204", -> - @res.sendStatus - .calledWith(204) - .should.equal true diff --git a/services/web/test/unit/coffee/History/HistoryManagerTests.coffee b/services/web/test/unit/coffee/History/HistoryManagerTests.coffee deleted file mode 100644 index ab9d54b8d9..0000000000 --- a/services/web/test/unit/coffee/History/HistoryManagerTests.coffee +++ /dev/null @@ -1,190 +0,0 @@ -chai = require('chai') -chai.should() -expect = chai.expect -sinon = require("sinon") -modulePath = "../../../../app/js/Features/History/HistoryManager" -SandboxedModule = require('sandboxed-module') - -describe "HistoryManager", -> - beforeEach -> - @callback = sinon.stub() - @user_id = "user-id-123" - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user_id) - @HistoryManager = SandboxedModule.require modulePath, requires: - "request" : @request = sinon.stub() - "settings-sharelatex": @settings = {} - "../User/UserGetter": @UserGetter = {} - @settings.apis = - trackchanges: - enabled: false - url: "http://trackchanges.example.com" - project_history: - url: "http://project_history.example.com" - - describe "initializeProject", -> - describe "with project history enabled", -> - beforeEach -> - @settings.apis.project_history.initializeHistoryForNewProjects = true - - describe "project history returns a successful response", -> - beforeEach -> - @overleaf_id = 1234 - @res = statusCode: 200 - @body = JSON.stringify(project: id: @overleaf_id) - @request.post = sinon.stub().callsArgWith(1, null, @res, @body) - - @HistoryManager.initializeProject @callback - - it "should call the project history api", -> - @request.post.calledWith( - url: "#{@settings.apis.project_history.url}/project" - ).should.equal true - - it "should return the callback with the overleaf id", -> - @callback.calledWithExactly(null, { @overleaf_id }).should.equal true - - describe "project history returns a response without the project id", -> - beforeEach -> - @res = statusCode: 200 - @body = JSON.stringify(project: {}) - @request.post = sinon.stub().callsArgWith(1, null, @res, @body) - - @HistoryManager.initializeProject @callback - - it "should return the callback with an error", -> - @callback - .calledWith(sinon.match.has("message", "project-history did not provide an id")) - .should.equal true - - describe "project history returns a unsuccessful response", -> - beforeEach -> - @res = statusCode: 404 - @request.post = sinon.stub().callsArgWith(1, null, @res) - - @HistoryManager.initializeProject @callback - - it "should return the callback with an error", -> - @callback - .calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404")) - .should.equal true - - describe "project history errors", -> - beforeEach -> - @error = sinon.stub() - @request.post = sinon.stub().callsArgWith(1, @error) - - @HistoryManager.initializeProject @callback - - it "should return the callback with the error", -> - @callback.calledWithExactly(@error).should.equal true - - describe "with project history disabled", -> - beforeEach -> - @settings.apis.project_history.initializeHistoryForNewProjects = false - @HistoryManager.initializeProject @callback - - it "should return the callback", -> - @callback.calledWithExactly().should.equal true - - describe "injectUserDetails", -> - beforeEach -> - @user1 = { - _id: @user_id1 = "123456" - first_name: "Jane", - last_name: "Doe" - email: "jane@example.com" - } - @user1_view = { - id: @user_id1 - first_name: "Jane", - last_name: "Doe" - email: "jane@example.com" - } - @user2 = { - _id: @user_id2 = "abcdef" - first_name: "John", - last_name: "Doe" - email: "john@example.com" - } - @user2_view = { - id: @user_id2 - first_name: "John", - last_name: "Doe" - email: "john@example.com" - } - @UserGetter.getUsers = sinon.stub().yields(null, [@user1, @user2]) - - describe "with a diff", -> - it "should turn user_ids into user objects", (done) -> - @HistoryManager.injectUserDetails { - diff: [{ - i: "foo" - meta: - users: [@user_id1] - }, { - i: "bar" - meta: - users: [@user_id2] - }] - }, (error, diff) => - expect(error).to.be.null - expect(diff.diff[0].meta.users).to.deep.equal [@user1_view] - expect(diff.diff[1].meta.users).to.deep.equal [@user2_view] - done() - - it "should leave user objects", (done) -> - @HistoryManager.injectUserDetails { - diff: [{ - i: "foo" - meta: - users: [@user1_view] - }, { - i: "bar" - meta: - users: [@user_id2] - }] - }, (error, diff) => - expect(error).to.be.null - expect(diff.diff[0].meta.users).to.deep.equal [@user1_view] - expect(diff.diff[1].meta.users).to.deep.equal [@user2_view] - done() - - describe "with a list of updates", -> - it "should turn user_ids into user objects", (done) -> - @HistoryManager.injectUserDetails { - updates: [{ - fromV: 5 - toV: 8 - meta: - users: [@user_id1] - }, { - fromV: 4 - toV: 5 - meta: - users: [@user_id2] - }] - }, (error, updates) => - expect(error).to.be.null - expect(updates.updates[0].meta.users).to.deep.equal [@user1_view] - expect(updates.updates[1].meta.users).to.deep.equal [@user2_view] - done() - - it "should leave user objects", (done) -> - @HistoryManager.injectUserDetails { - updates: [{ - fromV: 5 - toV: 8 - meta: - users: [@user1_view] - }, { - fromV: 4 - toV: 5 - meta: - users: [@user_id2] - }] - }, (error, updates) => - expect(error).to.be.null - expect(updates.updates[0].meta.users).to.deep.equal [@user1_view] - expect(updates.updates[1].meta.users).to.deep.equal [@user2_view] - done() \ No newline at end of file diff --git a/services/web/test/unit/coffee/History/RestoreManagerTests.coffee b/services/web/test/unit/coffee/History/RestoreManagerTests.coffee deleted file mode 100644 index 012c720a2b..0000000000 --- a/services/web/test/unit/coffee/History/RestoreManagerTests.coffee +++ /dev/null @@ -1,118 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -expect = require('chai').expect -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/History/RestoreManager' -Errors = require '../../../../app/js/Features/Errors/Errors' -tk = require("timekeeper") -moment = require('moment') - -describe 'RestoreManager', -> - beforeEach -> - tk.freeze Date.now() # freeze the time for these tests - @RestoreManager = SandboxedModule.require modulePath, requires: - '../../infrastructure/FileWriter': @FileWriter = {} - '../Uploads/FileSystemImportManager': @FileSystemImportManager = {} - '../Project/ProjectLocator': @ProjectLocator = {} - '../Errors/Errors': Errors - '../Project/ProjectEntityHandler': @ProjectEntityHandler = {} - '../Editor/EditorController': @EditorController = {} - 'logger-sharelatex': @logger = {log: sinon.stub(), err: sinon.stub()} - @user_id = 'mock-user-id' - @project_id = 'mock-project-id' - @version = 42 - @callback = sinon.stub() - - afterEach -> - tk.reset() - - describe 'restoreFileFromV2', -> - beforeEach -> - @RestoreManager._writeFileVersionToDisk = sinon.stub().yields(null, @fsPath = "/tmp/path/on/disk") - @RestoreManager._findOrCreateFolder = sinon.stub().yields(null, @folder_id = 'mock-folder-id') - @FileSystemImportManager.addEntity = sinon.stub().yields(null, @entity = 'mock-entity') - - describe "with a file not in a folder", -> - beforeEach -> - @pathname = 'foo.tex' - @RestoreManager.restoreFileFromV2 @user_id, @project_id, @version, @pathname, @callback - - it 'should write the file version to disk', -> - @RestoreManager._writeFileVersionToDisk - .calledWith(@project_id, @version, @pathname) - .should.equal true - - it 'should find the root folder', -> - @RestoreManager._findOrCreateFolder - .calledWith(@project_id, "") - .should.equal true - - it 'should add the entity', -> - @FileSystemImportManager.addEntity - .calledWith(@user_id, @project_id, @folder_id, 'foo.tex', @fsPath, false) - .should.equal true - - it 'should call the callback with the entity', -> - @callback.calledWith(null, @entity).should.equal true - - describe "with a file in a folder", -> - beforeEach -> - @pathname = 'foo/bar.tex' - @RestoreManager.restoreFileFromV2 @user_id, @project_id, @version, @pathname, @callback - - it 'should find the folder', -> - @RestoreManager._findOrCreateFolder - .calledWith(@project_id, "foo") - .should.equal true - - it 'should add the entity by its basename', -> - @FileSystemImportManager.addEntity - .calledWith(@user_id, @project_id, @folder_id, 'bar.tex', @fsPath, false) - .should.equal true - - describe '_findOrCreateFolder', -> - beforeEach -> - @EditorController.mkdirp = sinon.stub().yields(null, [], {_id: @folder_id = 'mock-folder-id'}) - @RestoreManager._findOrCreateFolder @project_id, 'folder/name', @callback - - it 'should look up or create the folder', -> - @EditorController.mkdirp - .calledWith(@project_id, 'folder/name') - .should.equal true - - it 'should return the folder_id', -> - @callback.calledWith(null, @folder_id).should.equal true - - - describe '_addEntityWithUniqueName', -> - beforeEach -> - @addEntityWithName = sinon.stub() - @name = 'foo.tex' - - describe 'with a valid name', -> - beforeEach -> - @addEntityWithName.yields(null, @entity = 'mock-entity') - @RestoreManager._addEntityWithUniqueName @addEntityWithName, @name, @callback - - it 'should add the entity', -> - @addEntityWithName.calledWith(@name).should.equal true - - it 'should return the entity', -> - @callback.calledWith(null, @entity).should.equal true - - describe "with an invalid name", -> - beforeEach -> - @addEntityWithName.onFirstCall().yields(new Errors.InvalidNameError()) - @addEntityWithName.onSecondCall().yields(null, @entity = 'mock-entity') - @RestoreManager._addEntityWithUniqueName @addEntityWithName, @name, @callback - - it 'should try to add the entity with its original name', -> - @addEntityWithName.calledWith('foo.tex').should.equal true - - it 'should try to add the entity with a unique name', -> - date = moment(new Date()).format('Do MMM YY H:mm:ss') - @addEntityWithName.calledWith("foo (Restored on #{date}).tex").should.equal true - - it 'should return the entity', -> - @callback.calledWith(null, @entity).should.equal true diff --git a/services/web/test/unit/coffee/InactiveData/InactiveProjectManagerTests.coffee b/services/web/test/unit/coffee/InactiveData/InactiveProjectManagerTests.coffee deleted file mode 100644 index e38478bbff..0000000000 --- a/services/web/test/unit/coffee/InactiveData/InactiveProjectManagerTests.coffee +++ /dev/null @@ -1,105 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/InactiveData/InactiveProjectManager" -expect = require("chai").expect - -describe "InactiveProjectManager", -> - - beforeEach -> - - @settings = {} - @DocstoreManager = - unarchiveProject:sinon.stub() - archiveProject:sinon.stub() - @ProjectUpdateHandler = - markAsActive:sinon.stub() - markAsInactive:sinon.stub() - @ProjectGetter = - getProject:sinon.stub() - @TrackChangesManager = - archiveProject:sinon.stub() - @InactiveProjectManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": - log:-> - err:-> - "../Docstore/DocstoreManager":@DocstoreManager - "../Project/ProjectUpdateHandler":@ProjectUpdateHandler - "../Project/ProjectGetter":@ProjectGetter - "../TrackChanges/TrackChangesManager":@TrackChangesManager - "../../models/Project":{} - @project_id = "1234" - - describe "reactivateProjectIfRequired", -> - - beforeEach -> - @project = {active:false} - @ProjectGetter.getProject.callsArgWith(2, null, @project) - @ProjectUpdateHandler.markAsActive.callsArgWith(1) - - it "should call unarchiveProject", (done)-> - @DocstoreManager.unarchiveProject.callsArgWith(1) - @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> - @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true - @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal true - done() - - it "should not mark project as active if error with unarchinging", (done)-> - @DocstoreManager.unarchiveProject.callsArgWith(1, "error") - @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> - err.should.equal "error" - @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true - @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false - done() - - - it "should not call unarchiveProject if it is active", (done)-> - @project.active = true - @DocstoreManager.unarchiveProject.callsArgWith(1) - @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> - @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal false - @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false - done() - - - describe "deactivateProject", -> - - it "should call unarchiveProject and markAsInactive", (done)-> - @DocstoreManager.archiveProject.callsArgWith(1) - @TrackChangesManager.archiveProject.callsArgWith(1) - - @ProjectUpdateHandler.markAsInactive.callsArgWith(1) - - @InactiveProjectManager.deactivateProject @project_id, (err)=> - @DocstoreManager.archiveProject.calledWith(@project_id).should.equal true - # @TrackChangesManager.archiveProject.calledWith(@project_id).should.equal true - @ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal true - done() - - it "should not call markAsInactive if there was a problem archiving in docstore", (done)-> - @DocstoreManager.archiveProject.callsArgWith(1, "errorrr") - @TrackChangesManager.archiveProject.callsArgWith(1) - - @ProjectUpdateHandler.markAsInactive.callsArgWith(1) - - @InactiveProjectManager.deactivateProject @project_id, (err)=> - err.should.equal "errorrr" - @DocstoreManager.archiveProject.calledWith(@project_id).should.equal true - @ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal false - done() - - - # it "should not call markAsInactive if there was a problem archiving in track changes", (done)-> - # @DocstoreManager.archiveProject.callsArgWith(1) - # @TrackChangesManager.archiveProject.callsArgWith(1, "errorrr") - - # @ProjectUpdateHandler.markAsInactive.callsArgWith(1) - - # @InactiveProjectManager.deactivateProject @project_id, (err)=> - # err.should.equal "errorrr" - # @DocstoreManager.archiveProject.calledWith(@project_id).should.equal true - # @ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal false - # done() diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee deleted file mode 100644 index 30faf918f2..0000000000 --- a/services/web/test/unit/coffee/Institutions/InstitutionsAPITests.coffee +++ /dev/null @@ -1,205 +0,0 @@ -should = require('chai').should() -expect = require('chai').expect -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Institutions/InstitutionsAPI" -expect = require("chai").expect - -describe "InstitutionsAPI", -> - - beforeEach -> - @logger = err: sinon.stub(), log: -> - @settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } - @request = sinon.stub() - @ipMatcherNotification = read: @markAsReadIpMatcher = sinon.stub().callsArgWith(1, null) - @InstitutionsAPI = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger - "metrics-sharelatex": timeAsyncMethod: sinon.stub() - 'settings-sharelatex': @settings - 'request': @request - "../Notifications/NotificationsBuilder": - ipMatcherAffiliation: sinon.stub().returns(@ipMatcherNotification) - - @stubbedUser = - _id: "3131231" - name:"bob" - email:"hello@world.com" - @newEmail = "bob@bob.com" - - describe 'getInstitutionAffiliations', -> - it 'get affiliations', (done)-> - @institutionId = 123 - responseBody = ['123abc', '456def'] - @request.yields(null, { statusCode: 200 }, responseBody) - @InstitutionsAPI.getInstitutionAffiliations @institutionId, (err, body) => - should.not.exist(err) - @request.calledOnce.should.equal true - requestOptions = @request.lastCall.args[0] - expectedUrl = "v1.url/api/v2/institutions/#{@institutionId}/affiliations" - requestOptions.url.should.equal expectedUrl - requestOptions.method.should.equal 'GET' - should.not.exist(requestOptions.body) - body.should.equal responseBody - done() - - it 'handle empty response', (done)-> - @settings.apis = null - @InstitutionsAPI.getInstitutionAffiliations @institutionId, (err, body) => - should.not.exist(err) - expect(body).to.be.a 'Array' - body.length.should.equal 0 - done() - - describe 'getInstitutionLicences', -> - it 'get licences', (done)-> - @institutionId = 123 - responseBody = {"lag":"monthly","data":[{"key":"users","values":[{"x":"2018-01-01","y":1}]}]} - @request.yields(null, { statusCode: 200 }, responseBody) - startDate = '1417392000' - endDate = '1420848000' - @InstitutionsAPI.getInstitutionLicences @institutionId, startDate, endDate, 'monthly', (err, body) => - should.not.exist(err) - @request.calledOnce.should.equal true - requestOptions = @request.lastCall.args[0] - expectedUrl = "v1.url/api/v2/institutions/#{@institutionId}/institution_licences" - requestOptions.url.should.equal expectedUrl - requestOptions.method.should.equal 'GET' - requestOptions.body['start_date'].should.equal startDate - requestOptions.body['end_date'].should.equal endDate - requestOptions.body.lag.should.equal 'monthly' - body.should.equal responseBody - done() - - describe 'getUserAffiliations', -> - it 'get affiliations', (done)-> - responseBody = [{ foo: 'bar' }] - @request.callsArgWith(1, null, { statusCode: 201 }, responseBody) - @InstitutionsAPI.getUserAffiliations @stubbedUser._id, (err, body) => - should.not.exist(err) - @request.calledOnce.should.equal true - requestOptions = @request.lastCall.args[0] - expectedUrl = "v1.url/api/v2/users/#{@stubbedUser._id}/affiliations" - requestOptions.url.should.equal expectedUrl - requestOptions.method.should.equal 'GET' - should.not.exist(requestOptions.body) - body.should.equal responseBody - done() - - it 'handle error', (done)-> - body = errors: 'affiliation error message' - @request.callsArgWith(1, null, { statusCode: 503 }, body) - @InstitutionsAPI.getUserAffiliations @stubbedUser._id, (err) => - should.exist(err) - err.message.should.have.string 503 - err.message.should.have.string body.errors - done() - - it 'handle empty response', (done)-> - @settings.apis = null - @InstitutionsAPI.getUserAffiliations @stubbedUser._id, (err, body) => - should.not.exist(err) - expect(body).to.be.a 'Array' - body.length.should.equal 0 - done() - - describe 'addAffiliation', -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 201 }) - - it 'add affiliation', (done)-> - affiliationOptions = - university: { id: 1 } - role: 'Prof' - department: 'Math' - confirmedAt: new Date() - @InstitutionsAPI.addAffiliation @stubbedUser._id, @newEmail, affiliationOptions, (err)=> - should.not.exist(err) - @request.calledOnce.should.equal true - requestOptions = @request.lastCall.args[0] - expectedUrl = "v1.url/api/v2/users/#{@stubbedUser._id}/affiliations" - requestOptions.url.should.equal expectedUrl - requestOptions.method.should.equal 'POST' - - body = requestOptions.body - Object.keys(body).length.should.equal 5 - body.email.should.equal @newEmail - body.university.should.equal affiliationOptions.university - body.department.should.equal affiliationOptions.department - body.role.should.equal affiliationOptions.role - body.confirmedAt.should.equal affiliationOptions.confirmedAt - @markAsReadIpMatcher.calledOnce.should.equal true - done() - - it 'handle error', (done)-> - body = errors: 'affiliation error message' - @request.callsArgWith(1, null, { statusCode: 422 }, body) - @InstitutionsAPI.addAffiliation @stubbedUser._id, @newEmail, {}, (err)=> - should.exist(err) - err.message.should.have.string 422 - err.message.should.have.string body.errors - done() - - describe 'removeAffiliation', -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 404 }) - - it 'remove affiliation', (done)-> - @InstitutionsAPI.removeAffiliation @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @request.calledOnce.should.equal true - requestOptions = @request.lastCall.args[0] - expectedUrl = "v1.url/api/v2/users/#{@stubbedUser._id}/affiliations/remove" - requestOptions.url.should.equal expectedUrl - requestOptions.method.should.equal 'POST' - expect(requestOptions.body).to.deep.equal { email: @newEmail } - done() - - it 'handle error', (done)-> - @request.callsArgWith(1, null, { statusCode: 500 }) - @InstitutionsAPI.removeAffiliation @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - err.message.should.exist - done() - - describe 'deleteAffiliations', -> - it 'delete affiliations', (done)-> - @request.callsArgWith(1, null, { statusCode: 200 }) - @InstitutionsAPI.deleteAffiliations @stubbedUser._id, (err) => - should.not.exist(err) - @request.calledOnce.should.equal true - requestOptions = @request.lastCall.args[0] - expectedUrl = "v1.url/api/v2/users/#{@stubbedUser._id}/affiliations" - requestOptions.url.should.equal expectedUrl - requestOptions.method.should.equal 'DELETE' - done() - - it 'handle error', (done)-> - body = errors: 'affiliation error message' - @request.callsArgWith(1, null, { statusCode: 518 }, body) - @InstitutionsAPI.deleteAffiliations @stubbedUser._id, (err) => - should.exist(err) - err.message.should.have.string 518 - err.message.should.have.string body.errors - done() - - describe 'endorseAffiliation', -> - beforeEach -> - @request.callsArgWith(1, null, { statusCode: 204 }) - - it 'endorse affiliation', (done)-> - @InstitutionsAPI.endorseAffiliation @stubbedUser._id, @newEmail, 'Student','Physics', (err)=> - should.not.exist(err) - @request.calledOnce.should.equal true - requestOptions = @request.lastCall.args[0] - expectedUrl = "v1.url/api/v2/users/#{@stubbedUser._id}/affiliations/endorse" - requestOptions.url.should.equal expectedUrl - requestOptions.method.should.equal 'POST' - - body = requestOptions.body - Object.keys(body).length.should.equal 3 - body.email.should.equal @newEmail - body.role.should.equal 'Student' - body.department.should.equal 'Physics' - done() diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsControllerTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsControllerTests.coffee deleted file mode 100644 index f68f1de08b..0000000000 --- a/services/web/test/unit/coffee/Institutions/InstitutionsControllerTests.coffee +++ /dev/null @@ -1,71 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Institutions/InstitutionsController" -expect = require("chai").expect - -describe "InstitutionsController", -> - - beforeEach -> - @logger = err: sinon.stub(), log: -> - @host = "mit.edu".split('').reverse().join('') - @stubbedUser1 = - _id: "3131231" - name:"bob" - email:"hello@world.com" - emails: [ - {"email":"stubb1@mit.edu","reversedHostname":@host}, - {"email":"test@test.com","reversedHostname":"test.com"}, - {"email":"another@mit.edu","reversedHostname":@host} - ] - @stubbedUser2 = - _id: "3131232" - name:"test" - email:"hello2@world.com" - emails: [ - {"email":"subb2@mit.edu","reversedHostname":@host} - ] - - @getUsersByHostname = sinon.stub().callsArgWith(2, null, [ @stubbedUser1, @stubbedUser2 ]) - @addAffiliation = sinon.stub().callsArgWith(3, null) - @refreshFeatures = sinon.stub().callsArgWith(2, null) - @InstitutionsController = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': @logger - '../User/UserGetter': - getUsersByHostname: @getUsersByHostname - '../Institutions/InstitutionsAPI': - addAffiliation: @addAffiliation - '../Subscription/FeaturesUpdater': - refreshFeatures: @refreshFeatures - - @req = - body: hostname: 'mit.edu' - - @res = - send: sinon.stub() - json: sinon.stub() - @next = sinon.stub() - - describe 'affiliateUsers', -> - it 'should add affiliations for matching users', (done)-> - @res.sendStatus = (code) => - code.should.equal 200 - @getUsersByHostname.calledOnce.should.equal true - @addAffiliation.calledThrice.should.equal true - @addAffiliation.calledWith(@stubbedUser1._id, @stubbedUser1.emails[0].email).should.equal true - @addAffiliation.calledWith(@stubbedUser1._id, @stubbedUser1.emails[2].email).should.equal true - @addAffiliation.calledWith(@stubbedUser2._id, @stubbedUser2.emails[0].email).should.equal true - @refreshFeatures.calledWith(@stubbedUser1._id, true).should.equal true - @refreshFeatures.calledWith(@stubbedUser2._id, true).should.equal true - done() - @InstitutionsController.confirmDomain @req, @res, @next - - it 'should return errors if last affiliation cannot be added', (done)-> - @addAffiliation.onCall(2).callsArgWith(3, new Error("error")) - @next = (error) => - expect(error).to.exist - @getUsersByHostname.calledOnce.should.equal true - done() - @InstitutionsController.confirmDomain @req, @res, @next diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee deleted file mode 100644 index 5fe21627c2..0000000000 --- a/services/web/test/unit/coffee/Institutions/InstitutionsFeaturesTests.coffee +++ /dev/null @@ -1,110 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -expect = require('chai').expect -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Institutions/InstitutionsFeatures.js' - -describe 'InstitutionsFeatures', -> - - beforeEach -> - @InstitutionsGetter = getConfirmedInstitutions: sinon.stub() - @PlansLocator = findLocalPlanInSettings: sinon.stub() - @institutionPlanCode = 'institution_plan_code' - @InstitutionsFeatures = SandboxedModule.require modulePath, requires: - './InstitutionsGetter': @InstitutionsGetter - '../Subscription/PlansLocator': @PlansLocator - 'settings-sharelatex': institutionPlanCode: @institutionPlanCode - 'logger-sharelatex': - log:-> - err:-> - - @userId = '12345abcde' - - describe "hasLicence", -> - it 'should handle error', (done)-> - @InstitutionsGetter.getConfirmedInstitutions.yields(new Error('Nope')) - @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> - expect(error).to.exist - done() - - it 'should return false if user has no confirmed affiliations', (done) -> - institutions = [] - @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) - @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> - expect(error).to.not.exist - expect(hasLicence).to.be.false - done() - - it 'should return false if user has no paid affiliations', (done) -> - institutions = [ - { licence: 'free' } - ] - @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) - @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> - expect(error).to.not.exist - expect(hasLicence).to.be.false - done() - - it 'should return true if user has confirmed paid affiliation', (done)-> - institutions = [ - { licence: 'pro_plus' } - { licence: 'free' } - { licence: 'pro' } - { licence: null } - ] - @InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions) - @InstitutionsFeatures.hasLicence @userId, (error, hasLicence) -> - expect(error).to.not.exist - expect(hasLicence).to.be.true - done() - - describe "getInstitutionsFeatures", -> - beforeEach -> - @InstitutionsFeatures.getInstitutionsPlan = sinon.stub() - @testFeatures = features: { institution: 'all' } - @PlansLocator.findLocalPlanInSettings.withArgs(@institutionPlanCode).returns(@testFeatures) - - it 'should handle error', (done)-> - @InstitutionsFeatures.getInstitutionsPlan.yields(new Error('Nope')) - @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) -> - expect(error).to.exist - done() - - it 'should return no feaures if user has no plan code', (done) -> - @InstitutionsFeatures.getInstitutionsPlan.yields(null, null) - @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) -> - expect(error).to.not.exist - expect(features).to.deep.equal {} - done() - - it 'should return feaures if user has affiliations plan code', (done) -> - @InstitutionsFeatures.getInstitutionsPlan.yields(null, @institutionPlanCode) - @InstitutionsFeatures.getInstitutionsFeatures @userId, (error, features) => - expect(error).to.not.exist - expect(features).to.deep.equal @testFeatures.features - done() - - describe "getInstitutionsPlan", -> - beforeEach -> - @InstitutionsFeatures.hasLicence = sinon.stub() - - it 'should handle error', (done)-> - @InstitutionsFeatures.hasLicence.yields(new Error('Nope')) - @InstitutionsFeatures.getInstitutionsPlan @userId, (error) -> - expect(error).to.exist - done() - - it 'should return no plan if user has no licence', (done) -> - @InstitutionsFeatures.hasLicence.yields(null, false) - @InstitutionsFeatures.getInstitutionsPlan @userId, (error, plan) -> - expect(error).to.not.exist - expect(plan).to.equal null - done() - - it 'should return plan if user has licence', (done) -> - @InstitutionsFeatures.hasLicence.yields(null, true) - @InstitutionsFeatures.getInstitutionsPlan @userId, (error, plan) => - expect(error).to.not.exist - expect(plan).to.equal @institutionPlanCode - done() diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee deleted file mode 100644 index 0536136896..0000000000 --- a/services/web/test/unit/coffee/Institutions/InstitutionsGetterTests.coffee +++ /dev/null @@ -1,47 +0,0 @@ -SandboxedModule = require('sandboxed-module') -require('chai').should() -expect = require('chai').expect -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Institutions/InstitutionsGetter.js' - -describe 'InstitutionsGetter', -> - beforeEach -> - @UserGetter = getUserFullEmails: sinon.stub() - @InstitutionsGetter = SandboxedModule.require modulePath, requires: - '../User/UserGetter': @UserGetter - "../UserMembership/UserMembershipsHandler": @UserMembershipsHandler = {} - "../UserMembership/UserMembershipEntityConfigs": @UserMembershipEntityConfigs = {} - 'logger-sharelatex': - log:-> console.log(arguments) - err:-> - - @userId = '12345abcde' - - describe "getConfirmedInstitutions", -> - it 'filters unconfirmed affiliations', (done) -> - @userEmails = [ - { confirmedAt: null, affiliation: institution: { id: 123, confirmed: true } } - { confirmedAt: new Date(), affiliation: institution: { id: 456, confirmed: true } } - { confirmedAt: new Date(), affiliation: null } - { confirmedAt: new Date(), affiliation: institution: null } - { confirmedAt: new Date(), affiliation: institution: { id: 789, confirmed: false } } - ] - @UserGetter.getUserFullEmails.yields(null, @userEmails) - @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> - expect(error).to.not.exist - institutions.length.should.equal 1 - institutions[0].id.should.equal 456 - done() - - it 'should handle empty response', (done) -> - @UserGetter.getUserFullEmails.yields(null, []) - @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> - expect(error).to.not.exist - institutions.length.should.equal 0 - done() - - it 'should handle error', (done) -> - @UserGetter.getUserFullEmails.yields(new Error('Nope')) - @InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) -> - expect(error).to.exist - done() diff --git a/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee b/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee deleted file mode 100644 index eef62369fb..0000000000 --- a/services/web/test/unit/coffee/Institutions/InstitutionsManagerTests.coffee +++ /dev/null @@ -1,137 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Institutions/InstitutionsManager" -expect = require('chai').expect - -describe "InstitutionsManager", -> - beforeEach -> - @institutionId = 123 - @logger = log: -> - @user = {} - @getInstitutionAffiliations = sinon.stub() - @refreshFeatures = sinon.stub().yields() - @UserGetter = - getUsersByAnyConfirmedEmail: sinon.stub().yields() - getUser: sinon.stub().callsArgWith(1, null, @user) - @creator = - create: sinon.stub().callsArg(0) - @NotificationsBuilder = - featuresUpgradedByAffiliation: sinon.stub().returns(@creator) - redundantPersonalSubscription: sinon.stub().returns(@creator) - @SubscriptionLocator = - getUsersSubscription: sinon.stub().callsArg(1) - @institutionWithV1Data = - name: 'Wombat University' - @institution = - fetchV1Data: sinon.stub().callsArgWith(0, null, @institutionWithV1Data) - @InstitutionModel = - Institution: - findOne: sinon.stub().callsArgWith(1, null, @institution) - @subscriptionExec = sinon.stub().yields() - SubscriptionModel = - Subscription: - find: () => - populate: () => - exec: @subscriptionExec - @Mongo = - ObjectId: sinon.stub().returnsArg(0) - - @InstitutionsManager = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': @logger - './InstitutionsAPI': - getInstitutionAffiliations: @getInstitutionAffiliations - '../Subscription/FeaturesUpdater': - refreshFeatures: @refreshFeatures - '../User/UserGetter': @UserGetter - '../Notifications/NotificationsBuilder': @NotificationsBuilder - '../Subscription/SubscriptionLocator': @SubscriptionLocator - '../../models/Institution': @InstitutionModel - "../../models/Subscription": SubscriptionModel - '../../infrastructure/mongojs': @Mongo - - describe 'upgradeInstitutionUsers', -> - beforeEach -> - @user1Id = '123abc123abc123abc123abc' - @user2Id = '456def456def456def456def' - @affiliations = [ - { user_id: @user1Id } - { user_id: @user2Id } - ] - @user1 = - _id: @user1Id - @user2 = - _id: @user2Id - @subscription = - planCode: 'pro' - groupPlan: false - @UserGetter.getUser.withArgs(@user1Id).callsArgWith(1, null, @user1) - @UserGetter.getUser.withArgs(@user2Id).callsArgWith(1, null, @user2) - @SubscriptionLocator.getUsersSubscription.withArgs(@user2).callsArgWith(1, null, @subscription) - @refreshFeatures.withArgs(@user1Id).callsArgWith(2, null, {}, true) - @getInstitutionAffiliations.yields(null, @affiliations) - - it 'refresh all users Features', (done) -> - @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) => - should.not.exist(error) - sinon.assert.calledTwice(@refreshFeatures) - done() - - it "notifies users if their features have been upgraded", (done) -> - @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) => - should.not.exist(error) - sinon.assert.calledOnce(@NotificationsBuilder.featuresUpgradedByAffiliation) - sinon.assert.calledWith(@NotificationsBuilder.featuresUpgradedByAffiliation, @affiliations[0], @user1) - done() - - it "notifies users if they have a subscription that should be cancelled", (done) -> - @InstitutionsManager.upgradeInstitutionUsers @institutionId, (error) => - should.not.exist(error) - sinon.assert.calledOnce(@NotificationsBuilder.redundantPersonalSubscription) - sinon.assert.calledWith(@NotificationsBuilder.redundantPersonalSubscription, @affiliations[1], @user2) - done() - - - describe 'checkInstitutionUsers', -> - it 'check all users Features', (done) -> - affiliations = [ - { email: 'foo@bar.com' } - { email: 'baz@boo.edu' } - ] - stubbedUsers = [ - { - _id: '123abc123abc123abc123abc' - features: {collaborators: -1, trackChanges: true} - } - { - _id: '456def456def456def456def' - features: {collaborators: 10, trackChanges: false} - } - { - _id: '789def789def789def789def' - features: {collaborators: -1, trackChanges: false} - } - ] - @getInstitutionAffiliations.yields(null, affiliations) - @UserGetter.getUsersByAnyConfirmedEmail.yields(null, stubbedUsers) - @InstitutionsManager.checkInstitutionUsers @institutionId, (error, usersSummary) => - should.not.exist(error) - usersSummary.totalConfirmedUsers.should.equal 3 - usersSummary.totalConfirmedProUsers.should.equal 1 - usersSummary.totalConfirmedNonProUsers.should.equal 2 - expect(usersSummary.confirmedNonProUsers).to.deep.equal ['456def456def456def456def', '789def789def789def789def'] - done() - - describe 'getInstitutionUsersSubscriptions', -> - it 'returns all institution users subscriptions', (done) -> - stubbedUsers = [ - { user_id: '123abc123abc123abc123abc' } - { user_id: '456def456def456def456def' } - { user_id: '789def789def789def789def' } - ] - @getInstitutionAffiliations.yields(null, stubbedUsers) - @InstitutionsManager.getInstitutionUsersSubscriptions @institutionId, (error, subscriptions) => - should.not.exist(error) - sinon.assert.calledOnce(@subscriptionExec) - done() diff --git a/services/web/test/unit/coffee/Metadata/MetaControllerTests.coffee b/services/web/test/unit/coffee/Metadata/MetaControllerTests.coffee deleted file mode 100644 index 208a1d5c3e..0000000000 --- a/services/web/test/unit/coffee/Metadata/MetaControllerTests.coffee +++ /dev/null @@ -1,119 +0,0 @@ -chai = require('chai') -chai.should() -expect = chai.expect -sinon = require("sinon") -modulePath = "../../../../app/js/Features/Metadata/MetaController" -SandboxedModule = require('sandboxed-module') - - -describe 'MetaController', -> - beforeEach -> - @projectId = 'somekindofid' - @EditorRealTimeController = { - emitToRoom: sinon.stub() - } - @MetaHandler = { - getAllMetaForProject: sinon.stub() - getMetaForDoc: sinon.stub() - } - @MetadataController = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} - '../Editor/EditorRealTimeController': @EditorRealTimeController - './MetaHandler': @MetaHandler - - describe 'getMetadata', -> - beforeEach -> - @fakeLabels = {'somedoc': ['a_label']} - @MetaHandler.getAllMetaForProject = sinon.stub().callsArgWith(1, null, @fakeLabels) - @req = {params: {project_id: @projectId}} - @res = {json: sinon.stub()} - @next = sinon.stub() - - it 'should call MetaHandler.getAllMetaForProject', () -> - @MetadataController.getMetadata(@req, @res, @next) - @MetaHandler.getAllMetaForProject.callCount.should.equal 1 - @MetaHandler.getAllMetaForProject.calledWith(@projectId).should.equal true - - it 'should call not call next with an error', () -> - @MetadataController.getMetadata(@req, @res, @next) - @next.callCount.should.equal 0 - - it 'should send a json response', () -> - @MetadataController.getMetadata(@req, @res, @next) - @res.json.callCount.should.equal 1 - expect(@res.json.lastCall.args[0]).to.have.all.keys ['projectId', 'projectMeta'] - - describe 'when MetaHandler.getAllMetaForProject produces an error', -> - beforeEach -> - @MetaHandler.getAllMetaForProject = sinon.stub().callsArgWith(1, new Error('woops')) - @req = {params: {project_id: @projectId}} - @res = {json: sinon.stub()} - @next = sinon.stub() - - it 'should call MetaHandler.getAllMetaForProject', () -> - @MetadataController.getMetadata(@req, @res, @next) - @MetaHandler.getAllMetaForProject.callCount.should.equal 1 - @MetaHandler.getAllMetaForProject.calledWith(@projectId).should.equal true - - it 'should call next with an error', -> - @MetadataController.getMetadata(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should not send a json response', -> - @MetadataController.getMetadata(@req, @res, @next) - @res.json.callCount.should.equal 0 - - describe 'broadcastMetadataForDoc', -> - beforeEach -> - @MetaHandler.getMetaForDoc = sinon.stub().callsArgWith(2, null, @fakeLabels) - @EditorRealTimeController.emitToRoom = sinon.stub() - @docId = 'somedoc' - @req = {params: {project_id: @projectId, doc_id: @docId}} - @res = {sendStatus: sinon.stub()} - @next = sinon.stub() - - it 'should call MetaHandler.getMetaForDoc', () -> - @MetadataController.broadcastMetadataForDoc(@req, @res, @next) - @MetaHandler.getMetaForDoc.callCount.should.equal 1 - @MetaHandler.getMetaForDoc.calledWith(@projectId).should.equal true - - it 'should call not call next with an error', () -> - @MetadataController.broadcastMetadataForDoc(@req, @res, @next) - @next.callCount.should.equal 0 - - it 'should send a success response', () -> - @MetadataController.broadcastMetadataForDoc(@req, @res, @next) - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(200).should.equal true - - it 'should emit a message to room', () -> - @MetadataController.broadcastMetadataForDoc(@req, @res, @next) - @EditorRealTimeController.emitToRoom.callCount.should.equal 1 - lastCall = @EditorRealTimeController.emitToRoom.lastCall - expect(lastCall.args[0]).to.equal @projectId - expect(lastCall.args[1]).to.equal 'broadcastDocMeta' - expect(lastCall.args[2]).to.have.all.keys ['docId', 'meta'] - - describe 'when MetaHandler.getMetaForDoc produces an error', -> - beforeEach -> - @MetaHandler.getMetaForDoc = sinon.stub().callsArgWith(2, new Error('woops')) - @EditorRealTimeController.emitToRoom = sinon.stub() - @docId = 'somedoc' - @req = {params: {project_id: @projectId, doc_id: @docId}} - @res = {json: sinon.stub()} - @next = sinon.stub() - - it 'should call MetaHandler.getMetaForDoc', () -> - @MetadataController.broadcastMetadataForDoc(@req, @res, @next) - @MetaHandler.getMetaForDoc.callCount.should.equal 1 - @MetaHandler.getMetaForDoc.calledWith(@projectId).should.equal true - - it 'should call next with an error', -> - @MetadataController.broadcastMetadataForDoc(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should not send a json response', -> - @MetadataController.broadcastMetadataForDoc(@req, @res, @next) - @res.json.callCount.should.equal 0 diff --git a/services/web/test/unit/coffee/Metadata/MetaHandlerTests.coffee b/services/web/test/unit/coffee/Metadata/MetaHandlerTests.coffee deleted file mode 100644 index ba975dc559..0000000000 --- a/services/web/test/unit/coffee/Metadata/MetaHandlerTests.coffee +++ /dev/null @@ -1,243 +0,0 @@ -chai = require('chai') -chai.should() -expect = chai.expect -sinon = require("sinon") -modulePath = "../../../../app/js/Features/Metadata/MetaHandler" -SandboxedModule = require('sandboxed-module') - - -describe 'MetaHandler', -> - beforeEach -> - @projectId = 'someprojectid' - @docId = 'somedocid' - @ProjectEntityHandler = { - getAllDocs: sinon.stub() - getDoc: sinon.stub() - } - @DocumentUpdaterHandler = { - flushDocToMongo: sinon.stub() - } - @packageMapping = - foo: [ - { - caption: '\\bar' - snippet: '\\bar' - meta: 'foo-cmd' - score: 12 - }, { - caption: '\\bat[]{}' - snippet: '\\bar[$1]{$2}' - meta: 'foo-cmd' - score: 10 - } - ], - baz: [ - { - caption: '\\longercommandtest{}' - snippet: '\\longercommandtest{$1}' - meta: 'baz-cmd' - score: 50 - } - ] - - @MetaHandler = SandboxedModule.require modulePath, requires: - '../Project/ProjectEntityHandler': @ProjectEntityHandler - '../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler - './packageMapping': @packageMapping - - describe 'extractMetaFromDoc', -> - beforeEach -> - @lines = [ - '\\usepackage{foo}' - '\\usepackage{amsmath, booktabs}' - 'one' - 'two' - 'three \\label{aaa}' - 'four five' - '\\label{bbb}' - 'six seven' - ] - - it 'should extract all the labels and packages', -> - docMeta = @MetaHandler.extractMetaFromDoc @lines - expect(docMeta).to.deep.equal { - labels: ['aaa', 'bbb'] - packages: - foo: [ - { - caption: '\\bar' - snippet: '\\bar' - meta: 'foo-cmd' - score: 12 - }, { - caption: '\\bat[]{}' - snippet: '\\bar[$1]{$2}' - meta: 'foo-cmd' - score: 10 - } - ] - } - - describe 'extractMetaFromProjectDocs', -> - beforeEach -> - @docs = - 'doc_one': - _id: 'id_one' - lines: ['one', '\\label{aaa} two', 'three'] - 'doc_two': - _id: 'id_two' - lines: ['four'] - 'doc_three': - _id: 'id_three' - lines: [ - '\\label{bbb}' - 'five six' - 'seven eight \\label{ccc} nine' - ] - 'doc_four': - _id: 'id_four' - lines: [ - '\\usepackage[width=\\textwidth]{baz}' - '\\usepackage{amsmath}' - ] - 'doc_five': - _id: 'id_five' - lines: [ - '\\usepackage{foo,baz}' - '\\usepackage[options=foo]{hello}' - 'some text' - '\\section{this}\\label{sec:intro}' - 'In Section \\ref{sec:intro} we saw' - 'nothing' - ] - - it 'should extract all metadata', -> - projectMeta = @MetaHandler.extractMetaFromProjectDocs @docs - expect(projectMeta).to.deep.equal { - 'id_one': {labels: ['aaa'], packages: {}} - 'id_two': {labels: [], packages: {}} - 'id_three': {labels: ['bbb', 'ccc'], packages: {}} - 'id_four': - labels: [] - packages: - baz: [{ - caption: '\\longercommandtest{}' - snippet: '\\longercommandtest{$1}' - meta: 'baz-cmd' - score: 50}] - 'id_five': - labels: ['sec:intro'] - packages: - foo: [ - { - caption: '\\bar' - snippet: '\\bar' - meta: 'foo-cmd' - score: 12 - }, { - caption: '\\bat[]{}' - snippet: '\\bar[$1]{$2}' - meta: 'foo-cmd' - score: 10 - } - ] - baz: [ - { - caption: '\\longercommandtest{}' - snippet: '\\longercommandtest{$1}' - meta: 'baz-cmd' - score: 50 - } - ] - } - - describe 'getMetaForDoc', -> - beforeEach -> - @fakeLines = ['\\usepackage{abc}', 'one', '\\label{aaa}', 'two'] - @fakeMeta = {labels: ['aaa'], packages: ['abc']} - @DocumentUpdaterHandler.flushDocToMongo = sinon.stub().callsArgWith 2, null - @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith 2, null, @fakeLines - @MetaHandler.extractMetaFromDoc = sinon.stub().returns @fakeMeta - @call = (callback) => - @MetaHandler.getMetaForDoc @projectId, @docId, callback - - it 'should not produce an error', (done) -> - @call (err, docMeta) => - expect(err).to.equal null - done() - - it 'should produce docMeta', (done) -> - @call (err, docMeta) => - expect(docMeta).to.equal @fakeMeta - done() - - it 'should call flushDocToMongo', (done) -> - @call (err, docMeta) => - @DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal 1 - @DocumentUpdaterHandler.flushDocToMongo.calledWith(@projectId, @docId).should.equal true - done() - - it 'should call getDoc', (done) -> - @call (err, docMeta) => - @ProjectEntityHandler.getDoc.callCount.should.equal 1 - @ProjectEntityHandler.getDoc.calledWith(@projectId, @docId).should.equal true - done() - - it 'should call extractMetaFromDoc', (done) -> - @call (err, docMeta) => - @MetaHandler.extractMetaFromDoc.callCount.should.equal 1 - @MetaHandler.extractMetaFromDoc.calledWith(@fakeLines).should.equal true - done() - - describe 'getAllMetaForProject', -> - beforeEach -> - @fakeDocs = - 'doc_one': - lines: [ - '\\usepackage[some-options,more=foo]{foo}' - '\\label{aaa}' - ] - - @fakeMeta = - labels: ['aaa'] - packages: - foo: [ - { - caption: '\\bar' - snippet: '\\bar' - meta: 'foo-cmd' - score: 12 - }, { - caption: '\\bat[]{}' - snippet: '\\bar[$1]{$2}' - meta: 'foo-cmd' - score: 10 - } - ] - @DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith 1, null - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith 1, null, @fakeDocs - @MetaHandler.extractMetaFromProjectDocs = sinon.stub().returns @fakeMeta - @call = (callback) => - @MetaHandler.getAllMetaForProject @projectId, callback - - it 'should not produce an error', (done) -> - @call (err, projectMeta) => - expect(err).to.equal null - done() - - it 'should produce projectMeta', (done) -> - @call (err, projectMeta) => - expect(projectMeta).to.equal @fakeMeta - done() - - it 'should call getAllDocs', (done) -> - @call (err, projectMeta) => - @ProjectEntityHandler.getAllDocs.callCount.should.equal 1 - @ProjectEntityHandler.getAllDocs.calledWith(@projectId).should.equal true - done() - - it 'should call extractMetaFromDoc', (done) -> - @call (err, docMeta) => - @MetaHandler.extractMetaFromProjectDocs.callCount.should.equal 1 - @MetaHandler.extractMetaFromProjectDocs.calledWith(@fakeDocs).should.equal true - done() diff --git a/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee deleted file mode 100644 index e2d78ec139..0000000000 --- a/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee +++ /dev/null @@ -1,39 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('chai').assert -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsBuilder.js' - -describe 'NotificationsBuilder', -> - user_id = "123nd3ijdks" - - beforeEach -> - @handler = - createNotification: sinon.stub().callsArgWith(6) - - @settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } - @body = {id: 1, name: 'stanford', enrolment_ad_html: 'v1 ad content'} - response = {statusCode: 200} - @request = sinon.stub().returns(@stubResponse).callsArgWith(1, null, response, @body) - @controller = SandboxedModule.require modulePath, requires: - "./NotificationsHandler":@handler - "settings-sharelatex":@settings - 'request': @request - "logger-sharelatex": - log:-> - err:-> - - it 'should call v1 and create affiliation notifications', (done)-> - ip = '192.168.0.1' - @controller.ipMatcherAffiliation(user_id).create ip, (callback)=> - @request.calledOnce.should.equal true - expectedOpts = - university_name: @body.name - content: @body.enrolment_ad_html - @handler.createNotification.calledWith( - user_id, - "ip-matched-affiliation-#{@body.id}", - "notification_ip_matched_affiliation", - expectedOpts - ).should.equal true - done() diff --git a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee deleted file mode 100644 index 126b223f04..0000000000 --- a/services/web/test/unit/coffee/Notifications/NotificationsControllerTests.coffee +++ /dev/null @@ -1,46 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsController.js' - - -describe 'NotificationsController', -> - user_id = "123nd3ijdks" - notification_id = "123njdskj9jlk" - - beforeEach -> - @handler = - getUserNotifications: sinon.stub().callsArgWith(1) - markAsRead: sinon.stub().callsArgWith(2) - @req = - params: - notification_id:notification_id - session: - user: - _id:user_id - i18n: - translate:-> - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@req.session.user._id) - @controller = SandboxedModule.require modulePath, requires: - "./NotificationsHandler":@handler - "underscore":@underscore = - map:(arr)-> return arr - 'logger-sharelatex': - log:-> - err:-> - '../Authentication/AuthenticationController': @AuthenticationController - - it 'should ask the handler for all unread notifications', (done)-> - allNotifications = [{_id: notification_id, user_id: user_id}] - @handler.getUserNotifications = sinon.stub().callsArgWith(1, null, allNotifications) - @controller.getAllUnreadNotifications @req, send:(body)=> - body.should.equal allNotifications - @handler.getUserNotifications.calledWith(user_id).should.equal true - done() - - it 'should send a delete request when a delete has been received to mark a notification', (done)-> - @controller.markNotificationAsRead @req, send:=> - @handler.markAsRead.calledWith(user_id, notification_id).should.equal true - done() diff --git a/services/web/test/unit/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsHandlerTests.coffee deleted file mode 100644 index 7ee3476b4b..0000000000 --- a/services/web/test/unit/coffee/Notifications/NotificationsHandlerTests.coffee +++ /dev/null @@ -1,103 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('chai').assert -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsHandler.js' -_ = require('underscore') - - -describe 'NotificationsHandler', -> - user_id = "123nd3ijdks" - notification_id = "123njdskj9jlk" - notificationUrl = "notification.sharelatex.testing" - - beforeEach -> - @request = sinon.stub().callsArgWith(1) - @handler = SandboxedModule.require modulePath, requires: - "settings-sharelatex": apis:{notifications:{url:notificationUrl}} - "request":@request - 'logger-sharelatex': - log:-> - err:-> - - describe "getUserNotifications", -> - it 'should get unread notifications', (done)-> - stubbedNotifications = [{_id: notification_id, user_id: user_id}] - @request.callsArgWith(1, null, {statusCode:200}, stubbedNotifications) - @handler.getUserNotifications user_id, (err, unreadNotifications)=> - stubbedNotifications.should.deep.equal unreadNotifications - getOpts = - uri: "#{notificationUrl}/user/#{user_id}" - json:true - timeout:1000 - method: "GET" - @request.calledWith(getOpts).should.equal true - done() - - it 'should return empty arrays if there are no notifications', -> - @request.callsArgWith(1, null, {statusCode:200}, null) - @handler.getUserNotifications user_id, (err, unreadNotifications)=> - unreadNotifications.length.should.equal 0 - - describe "markAsRead", -> - beforeEach -> - @key = "some key here" - - it 'should send a delete request when a delete has been received to mark a notification', (done)-> - @handler.markAsReadWithKey user_id, @key, => - opts = - uri: "#{notificationUrl}/user/#{user_id}" - json: - key:@key - timeout:1000 - method: "DELETE" - @request.calledWith(opts).should.equal true - done() - - - describe "createNotification", -> - beforeEach -> - @key = "some key here" - @messageOpts = {value:12344} - @templateKey = "renderThisHtml" - @expiry = null - - it "should post the message over", (done)-> - @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => - args = @request.args[0][0] - args.uri.should.equal "#{notificationUrl}/user/#{user_id}" - args.timeout.should.equal 1000 - expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, forceCreate:true} - assert.deepEqual(args.json, expectedJson) - done() - - describe 'when expiry date is supplied', -> - beforeEach -> - @key = "some key here" - @messageOpts = {value:12344} - @templateKey = "renderThisHtml" - @expiry = new Date() - - it 'should post the message over with expiry field', (done) -> - @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, => - args = @request.args[0][0] - args.uri.should.equal "#{notificationUrl}/user/#{user_id}" - args.timeout.should.equal 1000 - expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, expires: @expiry, forceCreate:true} - assert.deepEqual(args.json, expectedJson) - done() - - - - describe "markAsReadByKeyOnly", -> - beforeEach -> - @key = "some key here" - - it 'should send a delete request when a delete has been received to mark a notification', (done)-> - @handler.markAsReadByKeyOnly @key, => - opts = - uri: "#{notificationUrl}/key/#{@key}" - timeout:1000 - method: "DELETE" - @request.calledWith(opts).should.equal true - done() diff --git a/services/web/test/unit/coffee/PasswordReset/PasswordResetControllerTests.coffee b/services/web/test/unit/coffee/PasswordReset/PasswordResetControllerTests.coffee deleted file mode 100644 index fc5d491fcb..0000000000 --- a/services/web/test/unit/coffee/PasswordReset/PasswordResetControllerTests.coffee +++ /dev/null @@ -1,235 +0,0 @@ -should = require('chai').should() -expect = require("chai").expect -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/PasswordReset/PasswordResetController" -expect = require("chai").expect - -describe "PasswordResetController", -> - - beforeEach -> - - @settings = {} - @PasswordResetHandler = - generateAndEmailResetToken:sinon.stub() - setNewUserPassword:sinon.stub() - @RateLimiter = - addCount: sinon.stub() - @UserSessionsManager = - revokeAllUserSessions: sinon.stub().callsArgWith(2, null) - @AuthenticationManager = - validatePassword: sinon.stub() - @UserUpdater = - removeReconfirmFlag: sinon.stub().callsArgWith(1, null) - @PasswordResetController = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "./PasswordResetHandler":@PasswordResetHandler - "logger-sharelatex": log:-> - "../../infrastructure/RateLimiter":@RateLimiter - "../Authentication/AuthenticationController": @AuthenticationController = {} - "../Authentication/AuthenticationManager": @AuthenticationManager - "../User/UserGetter": @UserGetter = {} - "../User/UserSessionsManager": @UserSessionsManager - "../User/UserUpdater": @UserUpdater - - @email = "bob@bob.com " - @user_id = 'mock-user-id' - @token = "my security token that was emailed to me" - @password = "my new password" - @req = - body: - email:@email - passwordResetToken:@token - password:@password - i18n: - translate:-> - session: {} - query: {} - - @res = {} - - - describe "requestReset", -> - - it "should error if the rate limit is hit", (done)-> - @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, 'primary') - @RateLimiter.addCount.callsArgWith(1, null, false) - @res.send = (code)=> - code.should.equal 429 - @PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.trim()).should.equal false - done() - @PasswordResetController.requestReset @req, @res - - - it "should tell the handler to process that email", (done)-> - @RateLimiter.addCount.callsArgWith(1, null, true) - @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, 'primary') - @res.send = (code)=> - code.should.equal 200 - @PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.trim()).should.equal true - done() - @PasswordResetController.requestReset @req, @res - - it "should send a 500 if there is an error", (done)-> - @RateLimiter.addCount.callsArgWith(1, null, true) - @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, "error") - @res.send = (code)=> - code.should.equal 500 - done() - @PasswordResetController.requestReset @req, @res - - it "should send a 404 if the email doesn't exist", (done)-> - @RateLimiter.addCount.callsArgWith(1, null, true) - @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, null) - @res.send = (code)=> - code.should.equal 404 - done() - @PasswordResetController.requestReset @req, @res - - it "should send a 404 if the email is registered as a secondard email", (done)-> - @RateLimiter.addCount.callsArgWith(1, null, true) - @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, 'secondary') - @res.send = (code)=> - code.should.equal 404 - done() - @PasswordResetController.requestReset @req, @res - - it "should lowercase the email address", (done)-> - @email = "UPerCaseEMAIL@example.Com" - @req.body.email = @email - @RateLimiter.addCount.callsArgWith(1, null, true) - @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, 'primary') - @res.send = (code)=> - code.should.equal 200 - @PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.toLowerCase()).should.equal true - done() - @PasswordResetController.requestReset @req, @res - - describe "setNewUserPassword", -> - - beforeEach -> - @req.session.resetToken = @token - - it "should tell the user handler to reset the password", (done)-> - @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id) - @res.sendStatus = (code)=> - code.should.equal 200 - @PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true - done() - @PasswordResetController.setNewUserPassword @req, @res - - it "should send 404 if the token didn't work", (done)-> - @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, false, @user_id) - @res.sendStatus = (code)=> - code.should.equal 404 - done() - @PasswordResetController.setNewUserPassword @req, @res - - it "should return 400 (Bad Request) if there is no password", (done)-> - @req.body.password = "" - @PasswordResetHandler.setNewUserPassword.callsArgWith(2) - @res.sendStatus = (code)=> - code.should.equal 400 - @PasswordResetHandler.setNewUserPassword.called.should.equal false - done() - @PasswordResetController.setNewUserPassword @req, @res - - it "should return 400 (Bad Request) if there is no passwordResetToken", (done)-> - @req.body.passwordResetToken = "" - @PasswordResetHandler.setNewUserPassword.callsArgWith(2) - @res.sendStatus = (code)=> - code.should.equal 400 - @PasswordResetHandler.setNewUserPassword.called.should.equal false - done() - @PasswordResetController.setNewUserPassword @req, @res - - it "should return 400 (Bad Request) if the password is invalid", (done)-> - @req.body.password = "correct horse battery staple" - @AuthenticationManager.validatePassword = sinon.stub().returns { message: 'password contains invalid characters' } - @PasswordResetHandler.setNewUserPassword.callsArgWith(2) - @res.sendStatus = (code)=> - code.should.equal 400 - @PasswordResetHandler.setNewUserPassword.called.should.equal false - done() - @PasswordResetController.setNewUserPassword @req, @res - - it "should clear the session.resetToken", (done) -> - @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id) - @res.sendStatus = (code)=> - code.should.equal 200 - @req.session.should.not.have.property 'resetToken' - done() - @PasswordResetController.setNewUserPassword @req, @res - - it 'should clear sessions', (done) -> - @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id) - @res.sendStatus = (code)=> - @UserSessionsManager.revokeAllUserSessions.callCount.should.equal 1 - done() - @PasswordResetController.setNewUserPassword @req, @res - - it 'should call removeReconfirmFlag', (done) -> - @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id) - @res.sendStatus = (code)=> - @UserUpdater.removeReconfirmFlag.callCount.should.equal 1 - done() - @PasswordResetController.setNewUserPassword @req, @res - - describe 'when login_after is set', -> - - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, { email: "joe@example.com" }) - @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id = "user-id-123") - @req.body.login_after = "true" - @res.json = sinon.stub() - @AuthenticationController.afterLoginSessionSetup = sinon.stub().callsArgWith(2, null) - @AuthenticationController._getRedirectFromSession = sinon.stub().returns('/some/path') - - it "should login user if login_after is set", (done) -> - @PasswordResetController.setNewUserPassword @req, @res - @AuthenticationController.afterLoginSessionSetup.callCount.should.equal 1 - @AuthenticationController.afterLoginSessionSetup.calledWith( - @req, - {email: 'joe@example.com'} - ).should.equal true - @AuthenticationController._getRedirectFromSession.callCount.should.equal 1 - @res.json.callCount.should.equal 1 - @res.json.calledWith({redir: '/some/path'}).should.equal true - done() - - describe "renderSetPasswordForm", -> - - describe "with token in query-string", -> - beforeEach -> - @req.query.passwordResetToken = @token - - it "should set session.resetToken and redirect", (done) -> - @req.session.should.not.have.property 'resetToken' - @res.redirect = (path) => - path.should.equal '/user/password/set' - @req.session.resetToken.should.equal @token - done() - @PasswordResetController.renderSetPasswordForm(@req, @res) - - describe "without a token in query-string", -> - - describe "with token in session", -> - beforeEach -> - @req.session.resetToken = @token - - it "should render the page, passing the reset token", (done) -> - @res.render = (template_path, options) => - options.passwordResetToken.should.equal @req.session.resetToken - done() - @PasswordResetController.renderSetPasswordForm(@req, @res) - - describe "without a token in session", -> - - it "should redirect to the reset request page", (done) -> - @res.redirect = (path) => - path.should.equal "/user/password/reset" - @req.session.should.not.have.property 'resetToken' - done() - @PasswordResetController.renderSetPasswordForm(@req, @res) diff --git a/services/web/test/unit/coffee/PasswordReset/PasswordResetHandlerTests.coffee b/services/web/test/unit/coffee/PasswordReset/PasswordResetHandlerTests.coffee deleted file mode 100644 index 5c4fe4092b..0000000000 --- a/services/web/test/unit/coffee/PasswordReset/PasswordResetHandlerTests.coffee +++ /dev/null @@ -1,219 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/PasswordReset/PasswordResetHandler" -expect = require("chai").expect - -describe "PasswordResetHandler", -> - - beforeEach -> - - @settings = - siteUrl: "www.sharelatex.com" - @OneTimeTokenHandler = - getNewToken:sinon.stub() - getValueFromTokenAndExpire:sinon.stub() - @UserGetter = - getUserByMainEmail:sinon.stub() - getUser: sinon.stub() - getUserByAnyEmail: sinon.stub() - @EmailHandler = - sendEmail:sinon.stub() - @AuthenticationManager = - setUserPassword:sinon.stub() - setUserPasswordInV1:sinon.stub() - setUserPasswordInV2:sinon.stub() - @V1Api = - request: sinon.stub() - @PasswordResetHandler = SandboxedModule.require modulePath, requires: - "../User/UserGetter": @UserGetter - "../Security/OneTimeTokenHandler": @OneTimeTokenHandler - "../Email/EmailHandler":@EmailHandler - "../Authentication/AuthenticationManager":@AuthenticationManager - "../V1/V1Api": @V1Api - "settings-sharelatex": @settings - "logger-sharelatex": - log:-> - err:-> - @token = "12312321i" - @user_id = "user_id_here" - @user = - email : @email = "bob@bob.com" - @password = "my great secret password" - @callback = sinon.stub() - - - describe "generateAndEmailResetToken", -> - describe "when in ShareLaTeX", -> - it "should check the user exists", (done)-> - @UserGetter.getUserByMainEmail.callsArgWith(1) - @UserGetter.getUserByAnyEmail.callsArgWith(1) - @OneTimeTokenHandler.getNewToken.yields() - @PasswordResetHandler.generateAndEmailResetToken @user.email, (err, status)=> - should.equal(status, null) - done() - - it "should send the email with the token", (done)-> - @UserGetter.getUserByMainEmail.callsArgWith(1, null, @user) - @OneTimeTokenHandler.getNewToken.yields(null, @token) - @EmailHandler.sendEmail.callsArgWith(2) - @PasswordResetHandler.generateAndEmailResetToken @user.email, (err, status)=> - @EmailHandler.sendEmail.called.should.equal true - status.should.equal 'primary' - args = @EmailHandler.sendEmail.args[0] - args[0].should.equal "passwordResetRequested" - args[1].setNewPasswordUrl.should.equal "#{@settings.siteUrl}/user/password/set?passwordResetToken=#{@token}&email=#{encodeURIComponent(@user.email)}" - done() - - it "should return exists == null for a holdingAccount", (done) -> - @user.holdingAccount = true - @UserGetter.getUserByMainEmail.callsArgWith(1, null, @user) - @UserGetter.getUserByAnyEmail.callsArgWith(1) - @OneTimeTokenHandler.getNewToken.yields() - @PasswordResetHandler.generateAndEmailResetToken @user.email, (err, status)=> - should.equal(status, null) - done() - - describe "when in overleaf", -> - beforeEach -> - @settings.overleaf = true - - describe "when the email exists", -> - beforeEach -> - @V1Api.request.yields(null, {}, { user_id: 42 }) - @OneTimeTokenHandler.getNewToken.yields(null, @token) - @EmailHandler.sendEmail.yields() - @PasswordResetHandler.generateAndEmailResetToken @email, @callback - - it 'should call the v1 api for the user', -> - @V1Api.request.calledWith({ - url: "/api/v1/sharelatex/user_emails" - qs: - email: @email - expectedStatusCodes: [404] - }).should.equal true - - it 'should set the password token data to the user id and email', -> - @OneTimeTokenHandler.getNewToken - .calledWith('password', { - v1_user_id: 42 - }) - .should.equal true - - it 'should send an email with the token', -> - @EmailHandler.sendEmail.called.should.equal true - args = @EmailHandler.sendEmail.args[0] - args[0].should.equal "passwordResetRequested" - args[1].setNewPasswordUrl.should.equal "#{@settings.siteUrl}/user/password/set?passwordResetToken=#{@token}&email=#{encodeURIComponent(@user.email)}" - - it 'should return status == true', -> - @callback.calledWith(null, 'primary').should.equal true - - describe "when the email doesn't exist", -> - beforeEach -> - @V1Api.request = sinon.stub().yields(null, { statusCode: 404 }, {}) - @UserGetter.getUserByAnyEmail.callsArgWith(1) - @PasswordResetHandler.generateAndEmailResetToken @email, @callback - - it 'should not set the password token data', -> - @OneTimeTokenHandler.getNewToken - .called.should.equal false - - it 'should send an email with the token', -> - @EmailHandler.sendEmail.called.should.equal false - - it 'should return status == null', -> - @callback.calledWith(null, null).should.equal true - - describe "when the user isn't on v2", -> - beforeEach -> - @V1Api.request = sinon.stub().yields(null, { statusCode: 404 }, {}) - @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @user) - @PasswordResetHandler.generateAndEmailResetToken @email, @callback - - it 'should not set the password token data', -> - @OneTimeTokenHandler.getNewToken - .called.should.equal false - - it 'should not send an email with the token', -> - @EmailHandler.sendEmail.called.should.equal false - - it 'should return status == sharelatex', -> - @callback.calledWith(null, 'sharelatex').should.equal true - - describe "when the email is a secondary email", -> - beforeEach -> - @V1Api.request = sinon.stub().yields(null, { statusCode: 404 }, {}) - @user.overleaf = {id: 101} - @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @user) - @PasswordResetHandler.generateAndEmailResetToken @email, @callback - - it 'should not set the password token data', -> - @OneTimeTokenHandler.getNewToken - .called.should.equal false - - it 'should not send an email with the token', -> - @EmailHandler.sendEmail.called.should.equal false - - it 'should return status == secondary', -> - @callback.calledWith(null, 'secondary').should.equal true - - describe "setNewUserPassword", -> - describe "when no data is found", -> - beforeEach -> - @OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, null) - @PasswordResetHandler.setNewUserPassword @token, @password, @callback - - it 'should return exists == false', -> - @callback.calledWith(null, false).should.equal true - - describe 'when the data is an old style user_id', -> - beforeEach -> - @AuthenticationManager.setUserPassword.yields(null, true, @user_id) - @OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, @user_id) - @PasswordResetHandler.setNewUserPassword @token, @password, @callback - - it 'should call setUserPasswordInV2', -> - @AuthenticationManager.setUserPassword - .calledWith(@user_id, @password) - .should.equal true - - it 'should reset == true and the user_id', -> - @callback.calledWith(null, true, @user_id).should.equal true - - describe 'when the data is a new style user_id', -> - beforeEach -> - @AuthenticationManager.setUserPassword.yields(null, true, @user_id) - @OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, {@user_id}) - @PasswordResetHandler.setNewUserPassword @token, @password, @callback - - it 'should call setUserPasswordInV2', -> - @AuthenticationManager.setUserPassword - .calledWith(@user_id, @password) - .should.equal true - - it 'should reset == true and the user_id', -> - @callback.calledWith(null, true, @user_id).should.equal true - - describe 'when the data is v1 id', -> - beforeEach -> - @v1_user_id = 2345 - @AuthenticationManager.setUserPasswordInV1.yields(null, true) - @UserGetter.getUser.withArgs({'overleaf.id': @v1_user_id}).yields(null, { _id: @user_id }) - @OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, {@v1_user_id}) - @PasswordResetHandler.setNewUserPassword @token, @password, @callback - - it 'should call setUserPasswordInV1', -> - @AuthenticationManager.setUserPasswordInV1 - .calledWith(@v1_user_id, @password) - .should.equal true - - it 'should look up the user by v1 id for the v2 user id', -> - @UserGetter.getUser - .calledWith({'overleaf.id': @v1_user_id}) - .should.equal true - - it 'should reset == true and the user_id', -> - @callback.calledWith(null, true, @user_id).should.equal true diff --git a/services/web/test/unit/coffee/Project/DocLinesComparitorTests.coffee b/services/web/test/unit/coffee/Project/DocLinesComparitorTests.coffee deleted file mode 100644 index 1d6dd7541d..0000000000 --- a/services/web/test/unit/coffee/Project/DocLinesComparitorTests.coffee +++ /dev/null @@ -1,71 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -modulePath = "../../../../app/js/Features/Project/DocLinesComparitor.js" -SandboxedModule = require('sandboxed-module') - -describe 'doc lines comparitor', -> - - beforeEach -> - @comparitor = SandboxedModule.require modulePath, requires: - 'logger-sharelatex':{log:->} - - it 'should return true when the lines are the same', -> - lines1 = ["hello", "world"] - lines2 = ["hello", "world"] - result = @comparitor.areSame lines1, lines2 - result.should.equal true - - it 'should return false when the lines are different', -> - lines1 = ["hello", "world"] - lines2 = ["diff", "world"] - result = @comparitor.areSame lines1, lines2 - result.should.equal false - - it 'should return false when the lines are different', -> - lines1 = ["hello", "world"] - lines2 = ["hello", "wrld"] - result = @comparitor.areSame lines1, lines2 - result.should.equal false - - it 'should return true when the lines are same', -> - lines1 = ["hello", "world"] - lines2 = ['hello', "world"] - result = @comparitor.areSame lines1, lines2 - result.should.equal true - - it 'should return false if the doc lines are different in length', -> - lines1 = ["hello", "world"] - lines2 = ['hello', "world", "please"] - result = @comparitor.areSame lines1, lines2 - result.should.equal false - - it 'should return false if the first array is undefined', -> - lines1 = undefined - lines2 = ['hello', "world"] - result = @comparitor.areSame lines1, lines2 - result.should.equal false - - it 'should return false if the second array is undefined', -> - lines1 = ["hello"] - lines2 = undefined - result = @comparitor.areSame lines1, lines2 - result.should.equal false - - it 'should return false if the second array is not an array', -> - lines1 = ["hello"] - lines2 = "" - result = @comparitor.areSame lines1, lines2 - result.should.equal false - - it "should return true when comparing equal orchard docs", -> - lines1 = [{ text: "hello world" }] - lines2 = [{ text: "hello world" }] - result = @comparitor.areSame lines1, lines2 - result.should.equal true - - it "should return false when comparing different orchard docs", -> - lines1 = [{ text: "goodbye world" }] - lines2 = [{ text: "hello world" }] - result = @comparitor.areSame lines1, lines2 - result.should.equal false diff --git a/services/web/test/unit/coffee/Project/ProjectApiControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectApiControllerTests.coffee deleted file mode 100644 index ddf23c0bac..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectApiControllerTests.coffee +++ /dev/null @@ -1,41 +0,0 @@ -should = require('chai').should() -modulePath = "../../../../app/js/Features/Project/ProjectApiController" -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() - -describe 'Project api controller', -> - - beforeEach -> - @ProjectDetailsHandler = - getDetails : sinon.stub() - @controller = SandboxedModule.require modulePath, requires: - "./ProjectDetailsHandler":@ProjectDetailsHandler - 'logger-sharelatex': - log:-> - @project_id = "321l3j1kjkjl" - @req = - params: - project_id:@project_id - session: - destroy:sinon.stub() - @res = {} - @next = sinon.stub() - @projDetails = {name:"something"} - - - describe "getProjectDetails", -> - - it "should ask the project details handler for proj details", (done)-> - @ProjectDetailsHandler.getDetails.callsArgWith(1, null, @projDetails) - @res.json = (data)=> - @ProjectDetailsHandler.getDetails.calledWith(@project_id).should.equal true - data.should.deep.equal @projDetails - done() - @controller.getProjectDetails @req, @res - - - it "should send a 500 if there is an error", ()-> - @ProjectDetailsHandler.getDetails.callsArgWith(1, "error") - @controller.getProjectDetails @req, @res, @next - @next.calledWith("error").should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectCollabratecDetailsTest.coffee b/services/web/test/unit/coffee/Project/ProjectCollabratecDetailsTest.coffee deleted file mode 100644 index 4f2fe59c16..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectCollabratecDetailsTest.coffee +++ /dev/null @@ -1,270 +0,0 @@ -ObjectId = require("mongojs").ObjectId -Path = require "path" -SandboxedModule = require "sandboxed-module" -assert = require "assert" -chai = require "chai" -sinon = require "sinon" - -expect = chai.expect - -modulePath = Path.join __dirname, "../../../../app/js/Features/Project/ProjectCollabratecDetailsHandler" - -describe "ProjectCollabratecDetailsHandler", -> - beforeEach -> - @projectId = ObjectId("5bea8747c7bba6012fcaceb3") - @userId = ObjectId("5be316a9c7f6aa03802ea8fb") - @userId2 = ObjectId("5c1794b3f0e89b1d1c577eca") - @ProjectModel = {} - @ProjectCollabratecDetailsHandler = SandboxedModule.require modulePath, requires: - "../../models/Project": { Project: @ProjectModel } - @callback = sinon.stub() - - describe "initializeCollabratecProject", -> - - describe "when update succeeds", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields() - @ProjectCollabratecDetailsHandler.initializeCollabratecProject @projectId, @userId, "collabratec-document-id", "collabratec-private-group-id", @callback - - it "should update project model", -> - update = $set: { - collabratecUsers: [ { - user_id: @userId, - collabratec_document_id: "collabratec-document-id", - collabratec_privategroup_id: "collabratec-private-group-id" - } ] - } - expect(@ProjectModel.update).to.have.been.calledWith { _id: @projectId }, update, @callback - - describe "when update has error", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields("error") - @ProjectCollabratecDetailsHandler.initializeCollabratecProject @projectId, @userId, "collabratec-document-id", "collabratec-private-group-id", @callback - - it "should callback with error", -> - expect(@callback).to.have.been.calledWith("error") - - describe "with invalid args", -> - beforeEach -> - @ProjectModel.update = sinon.stub() - @ProjectCollabratecDetailsHandler.initializeCollabratecProject "bad-project-id", "bad-user-id", "collabratec-document-id", "collabratec-private-group-id", @callback - - it "should not update", -> - expect(@ProjectModel.update).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error - - describe "isLinkedCollabratecUserProject", -> - beforeEach -> - @ProjectModel.findOne = sinon.stub().yields() - - describe "when find succeeds", -> - describe "when user project found", -> - beforeEach -> - @ProjectModel.findOne = sinon.stub().yields(null, "project") - @ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject @projectId, @userId, @callback - - it "should call find with project and user id", -> - expect(@ProjectModel.findOne).to.have.been.calledWithMatch { - _id: ObjectId(@projectId) - collabratecUsers: $elemMatch: - user_id: ObjectId(@userId) - } - - it "should callback with true", -> - expect(@callback).to.have.been.calledWith null, true - - describe "when user project found", -> - beforeEach -> - @ProjectModel.findOne = sinon.stub().yields(null, null) - @ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject @projectId, @userId, @callback - - it "should callback with false", -> - expect(@callback).to.have.been.calledWith null, false - - describe "when find has error", -> - beforeEach -> - @ProjectModel.findOne = sinon.stub().yields("error") - @ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject @projectId, @userId, @callback - - it "should callback with error", -> - expect(@callback).to.have.been.calledWith "error" - - describe "with invalid args", -> - beforeEach -> - @ProjectModel.findOne = sinon.stub() - @ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject "bad-project-id", "bad-user-id", @callback - - it "should not update", -> - expect(@ProjectModel.findOne).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error - - describe "linkCollabratecUserProject", -> - - describe "when update succeeds", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields() - @ProjectCollabratecDetailsHandler.linkCollabratecUserProject @projectId, @userId, "collabratec-document-id", @callback - - it "should update project model", -> - query = - _id: @projectId - collabratecUsers: $not: $elemMatch: - collabratec_document_id: "collabratec-document-id" - user_id: @userId - update = $push: collabratecUsers: - collabratec_document_id: "collabratec-document-id" - user_id: @userId - expect(@ProjectModel.update).to.have.been.calledWith query, update, @callback - - describe "when update has error", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields("error") - @ProjectCollabratecDetailsHandler.linkCollabratecUserProject @projectId, @userId, "collabratec-document-id", @callback - - it "should callback with error", -> - expect(@callback).to.have.been.calledWith("error") - - describe "with invalid args", -> - beforeEach -> - @ProjectModel.update = sinon.stub() - @ProjectCollabratecDetailsHandler.linkCollabratecUserProject "bad-project-id", "bad-user-id", "collabratec-document-id", @callback - - it "should not update", -> - expect(@ProjectModel.update).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error - - describe "setCollabratecUsers", -> - beforeEach -> - @collabratecUsers = [ - { - user_id: @userId - collabratec_document_id: "collabratec-document-id-1" - collabratec_privategroup_id: "collabratec-private-group-id-1" - }, - { - user_id: @userId2 - collabratec_document_id: "collabratec-document-id-2" - collabratec_privategroup_id: "collabratec-private-group-id-2" - } - ] - - describe "when update succeeds", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields() - @ProjectCollabratecDetailsHandler.setCollabratecUsers @projectId, @collabratecUsers, @callback - - it "should update project model", -> - update = $set: { - collabratecUsers: @collabratecUsers - } - expect(@ProjectModel.update).to.have.been.calledWith { _id: @projectId }, update, @callback - - describe "when update has error", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields("error") - @ProjectCollabratecDetailsHandler.setCollabratecUsers @projectId, @collabratecUsers, @callback - - it "should callback with error", -> - expect(@callback).to.have.been.calledWith("error") - - describe "with invalid project_id", -> - beforeEach -> - @ProjectModel.update = sinon.stub() - @ProjectCollabratecDetailsHandler.setCollabratecUsers "bad-project-id", @collabratecUsers, @callback - - it "should not update", -> - expect(@ProjectModel.update).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error - - describe "with invalid user_id", -> - beforeEach -> - @collabratecUsers[1].user_id = "bad-user-id" - @ProjectModel.update = sinon.stub() - @ProjectCollabratecDetailsHandler.setCollabratecUsers @projectId, @collabratecUsers, @callback - - it "should not update", -> - expect(@ProjectModel.update).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error - - describe "unlinkCollabratecUserProject", -> - - describe "when update succeeds", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields() - @ProjectCollabratecDetailsHandler.unlinkCollabratecUserProject @projectId, @userId, @callback - - it "should update project model", -> - query = - _id: @projectId - update = $pull: collabratecUsers: - user_id: @userId - expect(@ProjectModel.update).to.have.been.calledWith query, update, @callback - - describe "when update has error", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields("error") - @ProjectCollabratecDetailsHandler.unlinkCollabratecUserProject @projectId, @userId, @callback - - it "should callback with error", -> - expect(@callback).to.have.been.calledWith("error") - - describe "with invalid args", -> - beforeEach -> - @ProjectModel.update = sinon.stub() - @ProjectCollabratecDetailsHandler.unlinkCollabratecUserProject "bad-project-id", "bad-user-id", @callback - - it "should not update", -> - expect(@ProjectModel.update).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error - - describe "updateCollabratecUserIds", -> - - describe "when update succeeds", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields() - @ProjectCollabratecDetailsHandler.updateCollabratecUserIds @userId, @userId2, @callback - - it "should update project model", -> - expect(@ProjectModel.update).to.have.been.calledWith { "collabratecUsers.user_id": @userId }, { $set: "collabratecUsers.$.user_id": @userId2 }, { multi: true}, @callback - - describe "when update has error", -> - beforeEach -> - @ProjectModel.update = sinon.stub().yields("error") - @ProjectCollabratecDetailsHandler.updateCollabratecUserIds @userId, @userId2, @callback - - it "should callback with error", -> - expect(@callback).to.have.been.calledWith("error") - - describe "with invalid old_user_id", -> - beforeEach -> - @ProjectModel.update = sinon.stub() - @ProjectCollabratecDetailsHandler.updateCollabratecUserIds "bad-user-id", @userId2, @callback - - it "should not update", -> - expect(@ProjectModel.update).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error - - describe "with invalid new_user_id", -> - beforeEach -> - @ProjectModel.update = sinon.stub() - @ProjectCollabratecDetailsHandler.updateCollabratecUserIds @userId, "bad-user-id", @callback - - it "should not update", -> - expect(@ProjectModel.update).not.to.have.beenCalled - - it "should callback with error", -> - expect(@callback.firstCall.args[0]).to.be.instanceOf Error \ No newline at end of file diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee deleted file mode 100644 index 497b8b30d1..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ /dev/null @@ -1,748 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Project/ProjectController" -expect = require("chai").expect -Errors = require "../../../../app/js/Features/Errors/Errors" - -describe "ProjectController", -> - - beforeEach -> - - @project_id = "123213jlkj9kdlsaj" - - @user = - _id:"588f3ddae8ebc1bac07c9fa4" - first_name: "bjkdsjfk" - features: {} - @settings = - apis: - chat: - url:"chat.com" - siteUrl: "mysite.com" - @brandVariationDetails = - id: "12" - active: true - brand_name: "The journal" - home_url: "http://www.thejournal.com/" - publish_menu_link_html: "Submit your paper to the The Journal" - @token = 'some-token' - @ProjectDeleter = - archiveProject: sinon.stub().callsArg(1) - deleteProject: sinon.stub().callsArg(2) - restoreProject: sinon.stub().callsArg(1) - findArchivedProjects: sinon.stub() - @ProjectDuplicator = - duplicate: sinon.stub().callsArgWith(3, null, {_id:@project_id}) - @ProjectCreationHandler = - createExampleProject: sinon.stub().callsArgWith(2, null, {_id:@project_id}) - createBasicProject: sinon.stub().callsArgWith(2, null, {_id:@project_id}) - @SubscriptionLocator = - getUsersSubscription: sinon.stub() - @LimitationsManager = - hasPaidSubscription: sinon.stub() - @TagsHandler = - getAllTags: sinon.stub() - @NotificationsHandler = - getUserNotifications: sinon.stub() - @UserModel = - findById: sinon.stub() - @AuthorizationManager = - getPrivilegeLevelForProject:sinon.stub() - @EditorController = - renameProject:sinon.stub() - @InactiveProjectManager = - reactivateProjectIfRequired:sinon.stub() - @ProjectUpdateHandler = - markAsOpened: sinon.stub() - @ReferencesSearchHandler = - indexProjectReferences: sinon.stub() - @ProjectGetter = - findAllUsersProjects: sinon.stub() - getProject: sinon.stub() - @AuthenticationController = - getLoggedInUser: sinon.stub().callsArgWith(1, null, @user) - getLoggedInUserId: sinon.stub().returns(@user._id) - getSessionUser: sinon.stub().returns(@user) - isUserLoggedIn: sinon.stub().returns(true) - @AnalyticsManager = - getLastOccurrence: sinon.stub() - @TokenAccessHandler = - getRequestToken: sinon.stub().returns(@token) - protectTokens: sinon.stub() - @CollaboratorsHandler = - userIsTokenMember: sinon.stub().callsArgWith(2, null, false) - @ProjectEntityHandler = {} - @NotificationBuilder = - ipMatcherAffiliation: sinon.stub().returns({create: sinon.stub()}) - @UserGetter = - getUser: sinon.stub().callsArgWith 2, null, {lastLoginIp: '192.170.18.2'} - getUserOrUserStubById: sinon.stub().callsArgWith 2, null, {} - @Modules = - hooks: - fire: sinon.stub() - @Features = - hasFeature: sinon.stub() - @BrandVariationsHandler = - getBrandVariationById: sinon.stub().callsArgWith 1, null, @brandVariationDetails - @getUserAffiliations = sinon.stub().callsArgWith(1, null, []) - - @ProjectController = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": - log:-> - err:-> - "metrics-sharelatex": - Timer:-> - done:-> - inc:-> - "./ProjectDeleter": @ProjectDeleter - "./ProjectDuplicator": @ProjectDuplicator - "./ProjectCreationHandler": @ProjectCreationHandler - "../Editor/EditorController": @EditorController - "../Subscription/SubscriptionLocator": @SubscriptionLocator - "../Subscription/LimitationsManager": @LimitationsManager - "../Tags/TagsHandler":@TagsHandler - "../Notifications/NotificationsHandler":@NotificationsHandler - "../../models/User":User:@UserModel - "../Authorization/AuthorizationManager":@AuthorizationManager - "../InactiveData/InactiveProjectManager":@InactiveProjectManager - "./ProjectUpdateHandler":@ProjectUpdateHandler - "../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler - "./ProjectGetter": @ProjectGetter - '../Authentication/AuthenticationController': @AuthenticationController - "../Analytics/AnalyticsManager": @AnalyticsManager - "../TokenAccess/TokenAccessHandler": @TokenAccessHandler - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler - "../../infrastructure/Modules": @Modules - "./ProjectEntityHandler": @ProjectEntityHandler - "../Errors/Errors": Errors - "../../infrastructure/Features": @Features - "../Notifications/NotificationsBuilder":@NotificationBuilder - "../User/UserGetter": @UserGetter - "../BrandVariations/BrandVariationsHandler": @BrandVariationsHandler - '../Institutions/InstitutionsAPI': - getUserAffiliations: @getUserAffiliations - '../V1/V1Handler': {} - - @projectName = "£12321jkj9ujkljds" - @req = - params: - Project_id: @project_id - headers: {} - connection: - remoteAddress: "192.170.18.1" - session: - user: @user - body: - projectName: @projectName - i18n: - translate:-> - ip: "192.170.18.1" - @res = - locals: - jsPath:"js path here" - setTimeout: sinon.stub() - - describe "updateProjectSettings", -> - it "should update the name", (done) -> - @EditorController.renameProject = sinon.stub().callsArg(2) - @req.body = - name: @name = "New name" - @res.sendStatus = (code) => - @EditorController.renameProject - .calledWith(@project_id, @name) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectSettings @req, @res - - it "should update the compiler", (done) -> - @EditorController.setCompiler = sinon.stub().callsArg(2) - @req.body = - compiler: @compiler = "pdflatex" - @res.sendStatus = (code) => - @EditorController.setCompiler - .calledWith(@project_id, @compiler) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectSettings @req, @res - - it "should update the imageName", (done) -> - @EditorController.setImageName = sinon.stub().callsArg(2) - @req.body = - imageName: @imageName = "texlive-1234.5" - @res.sendStatus = (code) => - @EditorController.setImageName - .calledWith(@project_id, @imageName) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectSettings @req, @res - - it "should update the spell check language", (done) -> - @EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2) - @req.body = - spellCheckLanguage: @languageCode = "fr" - @res.sendStatus = (code) => - @EditorController.setSpellCheckLanguage - .calledWith(@project_id, @languageCode) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectSettings @req, @res - - it "should update the root doc", (done) -> - @EditorController.setRootDoc = sinon.stub().callsArg(2) - @req.body = - rootDocId: @rootDocId = "root-doc-id" - @res.sendStatus = (code) => - @EditorController.setRootDoc - .calledWith(@project_id, @rootDocId) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectSettings @req, @res - - describe "updateProjectAdminSettings", -> - it "should update the public access level", (done) -> - @EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) - @req.body = - publicAccessLevel: @publicAccessLevel = "readonly" - @res.sendStatus = (code) => - @EditorController.setPublicAccessLevel - .calledWith(@project_id, @publicAccessLevel) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectAdminSettings @req, @res - - describe "deleteProject", -> - it "should tell the project deleter to archive when forever=false", (done)-> - @res.sendStatus = (code)=> - @ProjectDeleter.archiveProject.calledWith(@project_id).should.equal true - code.should.equal 200 - done() - @ProjectController.deleteProject @req, @res - - it "should tell the project deleter to delete when forever=true", (done)-> - @req.query = forever: "true" - @res.sendStatus = (code)=> - @ProjectDeleter.deleteProject.calledWith(@project_id, {deleterUser: @user, ipAddress:@req.ip}).should.equal true - code.should.equal 200 - done() - @ProjectController.deleteProject @req, @res - - describe "restoreProject", -> - it "should tell the project deleter", (done)-> - @res.sendStatus = (code)=> - @ProjectDeleter.restoreProject.calledWith(@project_id).should.equal true - code.should.equal 200 - done() - @ProjectController.restoreProject @req, @res - - describe "cloneProject", -> - it "should call the project duplicator", (done)-> - @res.send = (json)=> - @ProjectDuplicator.duplicate.calledWith(@user, @project_id, @projectName).should.equal true - json.project_id.should.equal @project_id - done() - @ProjectController.cloneProject @req, @res - - describe "newProject", -> - - it "should call the projectCreationHandler with createExampleProject", (done)-> - @req.body.template = "example" - @res.send = (json)=> - @ProjectCreationHandler.createExampleProject.calledWith(@user._id, @projectName).should.equal true - @ProjectCreationHandler.createBasicProject.called.should.equal false - done() - @ProjectController.newProject @req, @res - - - it "should call the projectCreationHandler with createBasicProject", (done)-> - @req.body.template = "basic" - @res.send = (json)=> - @ProjectCreationHandler.createExampleProject.called.should.equal false - @ProjectCreationHandler.createBasicProject.calledWith(@user._id, @projectName).should.equal true - done() - @ProjectController.newProject @req, @res - - describe "projectListPage", -> - - beforeEach -> - @tags = [{name:1, project_ids:["1","2","3"]}, {name:2, project_ids:["a","1"]}, {name:3, project_ids:["a", "b", "c", "d"]}] - @notifications = [{_id:'1',user_id:'2',templateKey:'3',messageOpts:'4',key:'5'}] - @projects = [ - {_id:1, lastUpdated:1, owner_ref: "user-1"}, - {_id:2, lastUpdated:2, owner_ref: "user-2", lastUpdatedBy: "user-1"} - ] - @collabertions = [ - {_id:5, lastUpdated:5, owner_ref: "user-1"} - ] - @readOnly = [ - {_id:3, lastUpdated:3, owner_ref: "user-1"} - ] - @tokenReadAndWrite = [ - {_id:6, lastUpdated:5, owner_ref: "user-4"} - ] - @tokenReadOnly = [ - {_id:7, lastUpdated:4, owner_ref: "user-5"} - ] - @allProjects = { - owned: @projects, - readAndWrite: @collabertions, - readOnly: @readOnly, - tokenReadAndWrite: @tokenReadAndWrite, - tokenReadOnly: @tokenReadOnly - } - - @users = - 'user-1': - first_name: 'James' - 'user-2': - first_name: 'Henry' - @users[@user._id] = @user # Owner - @UserModel.findById = (id, fields, callback) => - callback null, @users[id] - @UserGetter.getUserOrUserStubById = (id, fields, callback) => - callback null, @users[id] - - @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false) - @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {}) - @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {}) - @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @allProjects) - @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(undefined) # Without integration module hook, cb returns undefined - - it "should render the project/list page", (done)-> - @res.render = (pageName, opts)=> - pageName.should.equal "project/list" - done() - @ProjectController.projectListPage @req, @res - - it "should send the tags", (done)-> - @res.render = (pageName, opts)=> - opts.tags.length.should.equal @tags.length - done() - @ProjectController.projectListPage @req, @res - - it "should create trigger ip matcher notifications", (done)-> - @settings.overleaf = true - @res.render = (pageName, opts)=> - @NotificationBuilder.ipMatcherAffiliation.called.should.equal true - done() - @ProjectController.projectListPage @req, @res - - it "should send the projects", (done)-> - @res.render = (pageName, opts)=> - opts.projects.length.should.equal (@projects.length + @collabertions.length + @readOnly.length + @tokenReadAndWrite.length + @tokenReadOnly.length) - done() - @ProjectController.projectListPage @req, @res - - it "should send the user", (done)-> - @res.render = (pageName, opts)=> - opts.user.should.deep.equal @user - done() - @ProjectController.projectListPage @req, @res - - it "should inject the users", (done) -> - @res.render = (pageName, opts)=> - opts.projects[0].owner.should.equal (@users[@projects[0].owner_ref]) - opts.projects[1].owner.should.equal (@users[@projects[1].owner_ref]) - opts.projects[1].lastUpdatedBy.should.equal (@users[@projects[1].lastUpdatedBy]) - done() - @ProjectController.projectListPage @req, @res - - it 'should send hasSubscription == false when no subscription', (done) -> - @res.render = (pageName, opts)=> - opts.hasSubscription.should.equal false - done() - @ProjectController.projectListPage @req, @res - - it 'should send hasSubscription == true when there is a subscription', (done) -> - @LimitationsManager.hasPaidSubscription = sinon.stub().callsArgWith(1, null, true) - @res.render = (pageName, opts)=> - opts.hasSubscription.should.equal true - done() - @ProjectController.projectListPage @req, @res - - - describe 'front widget', (done) -> - beforeEach -> - @settings.overleaf = - front_chat_widget_room_id: 'chat-room-id' - - it 'should show for paid users', (done) -> - @user.features.github = true - @user.features.dropbox = true - @res.render = (pageName, opts)=> - opts.frontChatWidgetRoomId.should.equal @settings.overleaf.front_chat_widget_room_id - done() - @ProjectController.projectListPage @req, @res - - it 'should show for sample users', (done) -> - @user._id = '588f3ddae8ebc1bac07c9f00' # last two digits - @res.render = (pageName, opts)=> - opts.frontChatWidgetRoomId.should.equal @settings.overleaf.front_chat_widget_room_id - done() - @ProjectController.projectListPage @req, @res - - it 'should not show for non sample users', (done) -> - @user._id = '588f3ddae8ebc1bac07c9fff' # last two digits - @res.render = (pageName, opts)=> - expect(opts.frontChatWidgetRoomId).to.equal undefined - done() - @ProjectController.projectListPage @req, @res - - describe 'with overleaf-integration-web-module hook', -> - beforeEach -> - @V1Response = - projects: [ - { id: '123mockV1Id', title: 'mock title', updated_at: 1509616411, removed: false, archived: false } - { id: '456mockV1Id', title: 'mock title 2', updated_at: 1509616411, removed: true, archived: false } - ], - tags: [ - { name: 'mock tag', project_ids: ['123mockV1Id'] } - ] - @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(null, [@V1Response]) # Need to wrap response in array, as multiple hooks could fire - - it 'should include V1 projects', (done) -> - @res.render = (pageName, opts) => - opts.projects.length.should.equal ( - @projects.length + - @collabertions.length + - @readOnly.length + - @tokenReadAndWrite.length + - @tokenReadOnly.length + - @V1Response.projects.length - ) - opts.projects.forEach (p) -> - # Check properties correctly mapped from V1 - expect(p).to.have.property 'id' - expect(p).to.have.property 'name' - expect(p).to.have.property 'lastUpdated' - expect(p).to.have.property 'accessLevel' - expect(p).to.have.property 'archived' - done() - @ProjectController.projectListPage @req, @res - - it 'should include V1 tags', (done) -> - @res.render = (pageName, opts) => - opts.tags.length.should.equal (@tags.length + @V1Response.tags.length) - opts.tags.forEach (t) -> - expect(t).to.have.property 'name' - expect(t).to.have.property 'project_ids' - done() - @ProjectController.projectListPage @req, @res - - it 'should have isShowingV1Projects flag', (done) -> - @res.render = (pageName, opts) => - opts.isShowingV1Projects.should.equal true - done() - @ProjectController.projectListPage @req, @res - - describe "projectListPage with duplicate projects", -> - - beforeEach -> - @tags = [{name:1, project_ids:["1","2","3"]}, {name:2, project_ids:["a","1"]}, {name:3, project_ids:["a", "b", "c", "d"]}] - @notifications = [{_id:'1',user_id:'2',templateKey:'3',messageOpts:'4',key:'5'}] - @projects = [ - {_id:1, lastUpdated:1, owner_ref: "user-1"}, - {_id:2, lastUpdated:2, owner_ref: "user-2"} - ] - @collabertions = [ - {_id:5, lastUpdated:5, owner_ref: "user-1"} - ] - @readOnly = [ - {_id:3, lastUpdated:3, owner_ref: "user-1"} - ] - @tokenReadAndWrite = [ - {_id:6, lastUpdated:5, owner_ref: "user-4"} - ] - @tokenReadOnly = [ - {_id:6, lastUpdated:5, owner_ref: "user-4"} # Also in tokenReadAndWrite - {_id:7, lastUpdated:4, owner_ref: "user-5"} - ] - @allProjects = { - owned: @projects, - readAndWrite: @collabertions, - readOnly: @readOnly, - tokenReadAndWrite: @tokenReadAndWrite, - tokenReadOnly: @tokenReadOnly - } - - @users = - 'user-1': - first_name: 'James' - 'user-2': - first_name: 'Henry' - @users[@user._id] = @user # Owner - @UserModel.findById = (id, fields, callback) => - callback null, @users[id] - - @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false) - @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {}) - @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {}) - @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @allProjects) - @Modules.hooks.fire.withArgs('findAllV1Projects', @user._id).yields(undefined) # Without integration module hook, cb returns undefined - - it "should render the project/list page", (done)-> - @res.render = (pageName, opts)=> - pageName.should.equal "project/list" - done() - @ProjectController.projectListPage @req, @res - - it "should omit one of the projects", (done)-> - @res.render = (pageName, opts)=> - opts.projects.length.should.equal ( - @projects.length + @collabertions.length + @readOnly.length + @tokenReadAndWrite.length + @tokenReadOnly.length - 1 - ) - done() - @ProjectController.projectListPage @req, @res - - describe "renameProject", -> - beforeEach -> - @newProjectName = "my supper great new project" - @req.body.newProjectName = @newProjectName - - it "should call the editor controller", (done)-> - @EditorController.renameProject.callsArgWith(2) - @res.sendStatus = (code)=> - code.should.equal 200 - @EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true - done() - @ProjectController.renameProject @req, @res - - it "should send an error to next() if there is a problem", (done)-> - @EditorController.renameProject.callsArgWith(2, error = new Error("problem")) - next = (e)=> - e.should.equal error - done() - @ProjectController.renameProject @req, @res, next - - describe "loadEditor", -> - beforeEach -> - @settings.editorIsOpen = true - @project = - name:"my proj" - _id:"213123kjlkj" - owner_ref: '59fc84d5fbea77482d436e1b' - @brandedProject = - name:"my branded proj" - _id:"3252332" - owner_ref: '59fc84d5fbea77482d436e1b' - brandVariationId:"12" - @user = - _id: "588f3ddae8ebc1bac07c9fa4" - ace: - fontSize:"massive" - theme:"sexy" - email: "bob@bob.com" - @ProjectGetter.getProject.callsArgWith 2, null, @project - @UserModel.findById.callsArgWith(1, null, @user) - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 3, null, "owner" - @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() - @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) - @AnalyticsManager.getLastOccurrence.yields(null, {"mock": "event"}) - @ProjectUpdateHandler.markAsOpened.callsArgWith(1) - - it "should render the project/editor page", (done)-> - @res.render = (pageName, opts)=> - pageName.should.equal "project/editor" - done() - @ProjectController.loadEditor @req, @res - - it "should add user", (done)-> - @res.render = (pageName, opts)=> - opts.user.email.should.equal @user.email - done() - @ProjectController.loadEditor @req, @res - - it "should add on userSettings", (done)-> - @res.render = (pageName, opts)=> - opts.userSettings.fontSize.should.equal @user.ace.fontSize - opts.userSettings.editorTheme.should.equal @user.ace.theme - done() - @ProjectController.loadEditor @req, @res - - it "should render the closed page if the editor is closed", (done)-> - @settings.editorIsOpen = false - @res.render = (pageName, opts)=> - pageName.should.equal "general/closed" - done() - @ProjectController.loadEditor @req, @res - - it "should not render the page if the project can not be accessed", (done)-> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 3, null, null - @res.sendStatus = (resCode, opts)=> - resCode.should.equal 401 - done() - @ProjectController.loadEditor @req, @res - - it "should reactivateProjectIfRequired", (done)-> - @res.render = (pageName, opts)=> - @InactiveProjectManager.reactivateProjectIfRequired.calledWith(@project_id).should.equal true - done() - @ProjectController.loadEditor @req, @res - - it "should mark project as opened", (done)-> - @res.render = (pageName, opts)=> - @ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true - done() - @ProjectController.loadEditor @req, @res - - it "should call the brand variations handler for branded projects", (done)-> - @ProjectGetter.getProject.callsArgWith 2, null, @brandedProject - @res.render = (pageName, opts)=> - @BrandVariationsHandler.getBrandVariationById.calledWith().should.equal true - done() - @ProjectController.loadEditor @req, @res - - it "should not call the brand variations handler for unbranded projects", (done)-> - @res.render = (pageName, opts)=> - @BrandVariationsHandler.getBrandVariationById.called.should.equal false - done() - @ProjectController.loadEditor @req, @res - - it "should expose the brand variation details as locals for branded projects", (done)-> - @ProjectGetter.getProject.callsArgWith 2, null, @brandedProject - @res.render = (pageName, opts)=> - opts.brandVariation.should.deep.equal @brandVariationDetails - done() - @ProjectController.loadEditor @req, @res - - describe 'userProjectsJson', -> - beforeEach (done) -> - projects = [ - {archived: true, id: 'a', name: 'A', accessLevel: 'a', somethingElse: 1} - {archived: false, id: 'b', name: 'B', accessLevel: 'b', somethingElse: 1} - {archived: false, id: 'c', name: 'C', accessLevel: 'c', somethingElse: 1} - {archived: false, id: 'd', name: 'D', accessLevel: 'd', somethingElse: 1} - ] - @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, []) - @ProjectController._buildProjectList = sinon.stub().returns(projects) - @AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc' - done() - - it 'should produce a list of projects', (done) -> - @res.json = (data) => - expect(data).to.deep.equal { - projects: [ - {_id: 'b', name: 'B', accessLevel: 'b'}, - {_id: 'c', name: 'C', accessLevel: 'c'}, - {_id: 'd', name: 'D', accessLevel: 'd'} - ] - } - done() - @ProjectController.userProjectsJson @req, @res, @next - - describe 'projectEntitiesJson', -> - beforeEach () -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc' - @req.params = {Project_id: 'abcd'} - @project = { _id: 'abcd' } - @docs = [ - {path: '/things/b.txt', doc: true}, - {path: '/main.tex', doc: true} - ] - @files = [ - {path: '/things/a.txt'} - ] - @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @project) - @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files) - - it 'should produce a list of entities', (done) -> - @res.json = (data) => - expect(data).to.deep.equal { - project_id: 'abcd', - entities: [ - {path: '/main.tex', type: 'doc'}, - {path: '/things/a.txt', type: 'file'}, - {path: '/things/b.txt', type: 'doc'} - ] - } - expect(@ProjectGetter.getProject.callCount).to.equal 1 - expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 1 - done() - @ProjectController.projectEntitiesJson @req, @res, @next - - describe '_isInPercentageRollout', -> - before -> - @ids = [ - '5a05cd7621f9fe22be131740', - '5a05cd7821f9fe22be131741', - '5a05cd7921f9fe22be131742', - '5a05cd7a21f9fe22be131743', - '5a05cd7b21f9fe22be131744', - '5a05cd7c21f9fe22be131745', - '5a05cd7d21f9fe22be131746', - '5a05cd7e21f9fe22be131747', - '5a05cd7f21f9fe22be131748', - '5a05cd8021f9fe22be131749', - '5a05cd8021f9fe22be13174a', - '5a05cd8121f9fe22be13174b', - '5a05cd8221f9fe22be13174c', - '5a05cd8221f9fe22be13174d', - '5a05cd8321f9fe22be13174e', - '5a05cd8321f9fe22be13174f', - '5a05cd8421f9fe22be131750', - '5a05cd8421f9fe22be131751', - '5a05cd8421f9fe22be131752', - '5a05cd8521f9fe22be131753' - ] - - it 'should produce the expected results', -> - expect( - @ids.map (i) => - @ProjectController._isInPercentageRollout('abcd', i, 50) - ).to.deep.equal [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - true, - false, - true - ] - expect( - @ids.map (i) => - @ProjectController._isInPercentageRollout('efgh', i, 50) - ).to.deep.equal [ - false, - false, - false, - false, - true, - false, - false, - true, - false, - false, - true, - true, - true, - false, - true, - false, - true, - true, - false, - false - ] diff --git a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee deleted file mode 100644 index 2cb90b159c..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee +++ /dev/null @@ -1,315 +0,0 @@ -spies = require('chai-spies') -chai = require('chai').use(spies) -sinon = require("sinon") -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Project/ProjectCreationHandler.js" -SandboxedModule = require('sandboxed-module') -Settings = require('settings-sharelatex') -Path = require "path" -_ = require("underscore") - -describe 'ProjectCreationHandler', -> - ownerId = '4eecb1c1bffa66588e0000a1' - projectName = 'project name goes here' - project_id = "4eecaffcbffa66588e000008" - docId = '4eecb17ebffa66588e00003f' - rootFolderId = "234adfa3r2afe" - - beforeEach -> - @ProjectModel = class Project - constructor:(options = {})-> - @._id = project_id - @owner_ref = options.owner_ref - @name = options.name - @overleaf = - history: {} - save: sinon.stub().callsArg(0) - rootFolder:[{ - _id: rootFolderId - docs: [] - }] - @FolderModel = class Folder - constructor:(options)-> - {@name} = options - @ProjectEntityUpdateHandler = - addDoc: sinon.stub().callsArgWith(5, null, {_id: docId}) - addFile: sinon.stub().callsArg(6) - setRootDoc: sinon.stub().callsArg(2) - @ProjectDetailsHandler = - validateProjectName: sinon.stub().yields() - @HistoryManager = - initializeProject: sinon.stub().callsArg(0) - - @user = - first_name:"first name here" - last_name:"last name here" - ace: - spellCheckLanguage:"de" - - @User = findById:sinon.stub().callsArgWith(2, null, @user) - @callback = sinon.stub() - - @Settings = apis: { project_history: {} } - - @AnalyticsManager = recordEvent: sinon.stub() - - @handler = SandboxedModule.require modulePath, requires: - '../../models/User': User:@User - '../../models/Project':{Project:@ProjectModel} - '../../models/Folder':{Folder:@FolderModel} - '../History/HistoryManager': @HistoryManager - './ProjectEntityUpdateHandler':@ProjectEntityUpdateHandler - "./ProjectDetailsHandler":@ProjectDetailsHandler - "settings-sharelatex": @Settings - "../Analytics/AnalyticsManager": @AnalyticsManager - 'logger-sharelatex': {log:->} - "metrics-sharelatex": { - inc: ()->, - timeAsyncMethod: ()-> - } - - describe 'Creating a Blank project', -> - beforeEach -> - @overleaf_id = 1234 - @HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id }) - @ProjectModel::save = sinon.stub().callsArg(0) - - describe "successfully", -> - it "should save the project", (done)-> - @handler.createBlankProject ownerId, projectName, => - @ProjectModel::save.called.should.equal true - done() - - it "should return the project in the callback", (done)-> - @handler.createBlankProject ownerId, projectName, (err, project)-> - project.name.should.equal projectName - (project.owner_ref + "").should.equal ownerId - done() - - it "should initialize the project overleaf if history id not provided", (done)-> - @handler.createBlankProject ownerId, projectName, done - @HistoryManager.initializeProject.calledWith().should.equal true - - it "should set the overleaf id if overleaf id not provided", (done)-> - @handler.createBlankProject ownerId, projectName, (err, project)=> - project.overleaf.history.id.should.equal @overleaf_id - done() - - it "should set the overleaf id if overleaf id provided", (done)-> - overleaf_id = 2345 - attributes = - overleaf: - history: - id: overleaf_id - @handler.createBlankProject ownerId, projectName, attributes, (err, project)-> - project.overleaf.history.id.should.equal overleaf_id - done() - - it "should set the language from the user", (done)-> - @handler.createBlankProject ownerId, projectName, (err, project)-> - project.spellCheckLanguage.should.equal "de" - done() - - it "should set the imageName to currentImageName if set and no imageName attribute", (done) -> - @Settings.currentImageName = "mock-image-name" - @handler.createBlankProject ownerId, projectName, (err, project)=> - project.imageName.should.equal @Settings.currentImageName - done() - - it "should not set the imageName if no currentImageName", (done) -> - @Settings.currentImageName = null - @handler.createBlankProject ownerId, projectName, (err, project)=> - expect(project.imageName).to.not.exist - done() - - it "should set the imageName to the attribute value if set and not overwrite it with the currentImageName", (done) -> - @Settings.currentImageName = "mock-image-name" - attributes = - imageName: "attribute-image-name" - @handler.createBlankProject ownerId, projectName, attributes, (err, project)=> - project.imageName.should.equal attributes.imageName - done() - - it "should not set the overleaf.history.display if not configured in settings", (done) -> - @Settings.apis.project_history.displayHistoryForNewProjects = false - @handler.createBlankProject ownerId, projectName, (err, project)=> - expect(project.overleaf.history.display).to.not.exist - done() - - it "should set the overleaf.history.display if configured in settings", (done) -> - @Settings.apis.project_history.displayHistoryForNewProjects = true - @handler.createBlankProject ownerId, projectName, (err, project)=> - expect(project.overleaf.history.display).to.equal true - done() - - it "should send a project-created event to analytics", (done) -> - @handler.createBlankProject ownerId, projectName, (err, project) => - expect(@AnalyticsManager.recordEvent.callCount).to.equal 1 - expect( - @AnalyticsManager.recordEvent.calledWith(ownerId, 'project-created') - ).to.equal true - done() - - it "should send a project-imported event when importing a project", (done) -> - @handler.createBlankProject ownerId, projectName, 1234, (err, project) => - expect(@AnalyticsManager.recordEvent.callCount).to.equal 1 - expect( - @AnalyticsManager.recordEvent.calledWith(ownerId, 'project-imported') - ).to.equal true - done() - - - describe "with an error", -> - beforeEach -> - @ProjectModel::save = sinon.stub().callsArgWith(0, new Error("something went wrong")) - @handler.createBlankProject ownerId, projectName, @callback - - it 'should return the error to the callback', -> - should.exist @callback.args[0][0] - - describe "with an invalid name", -> - beforeEach -> - @ProjectDetailsHandler.validateProjectName = sinon.stub().yields(new Error("bad name")) - @handler.createBlankProject ownerId, projectName, @callback - - it 'should return the error to the callback', -> - should.exist @callback.args[0][0] - - it 'should not try to create the project', -> - @ProjectModel::save.called.should.equal false - - - describe 'Creating a basic project', -> - beforeEach -> - @project = new @ProjectModel() - @handler._buildTemplate = (template_name, user, project_name, callback) -> - if template_name == "mainbasic.tex" - return callback(null, ["mainbasic.tex", "lines"]) - throw new Error("unknown template: #{template_name}") - sinon.spy @handler, "_buildTemplate" - @handler.createBlankProject = sinon.stub().callsArgWith(2, null, @project) - @handler._createRootDoc = sinon.stub().callsArgWith(3, null, @project) - @handler.createBasicProject(ownerId, projectName, @callback) - - it "should create a blank project first", -> - @handler.createBlankProject.calledWith(ownerId, projectName) - .should.equal true - - it 'should create the root document', -> - @handler._createRootDoc - .calledWith(@project, ownerId, ["mainbasic.tex", "lines"]) - .should.equal true - - it 'should build the mainbasic.tex template', -> - @handler._buildTemplate - .calledWith("mainbasic.tex", ownerId, projectName) - .should.equal true - - describe 'Creating a project from a snippet', -> - beforeEach -> - @project = new @ProjectModel - @handler.createBlankProject = sinon.stub().callsArgWith(2, null, @project) - @handler._createRootDoc = sinon.stub().callsArgWith(3, null, @project) - @handler.createProjectFromSnippet(ownerId, projectName, ["snippet line 1", "snippet line 2"], @callback) - - it "should create a blank project first", -> - @handler.createBlankProject.calledWith(ownerId, projectName) - .should.equal true - - it 'should create the root document', -> - @handler._createRootDoc - .calledWith(@project, ownerId, ["snippet line 1", "snippet line 2"]) - .should.equal true - - describe 'Creating an example project', -> - beforeEach -> - @project = new @ProjectModel() - @handler._buildTemplate = (template_name, user, project_name, callback) -> - if template_name == "main.tex" - return callback(null, ["main.tex", "lines"]) - if template_name == "references.bib" - return callback(null, ["references.bib", "lines"]) - throw new Error("unknown template: #{template_name}") - sinon.spy @handler, "_buildTemplate" - @handler.createBlankProject = sinon.stub().callsArgWith(2, null, @project) - @handler._createRootDoc = sinon.stub().callsArgWith(3, null, @project) - @handler.createExampleProject(ownerId, projectName, @callback) - - it "should create a blank project first", -> - @handler.createBlankProject.calledWith(ownerId, projectName) - .should.equal true - - it 'should create the root document', -> - @handler._createRootDoc - .calledWith(@project, ownerId, ["main.tex", "lines"]) - .should.equal true - - it 'should insert references.bib', -> - @ProjectEntityUpdateHandler.addDoc - .calledWith(project_id, rootFolderId, "references.bib", ["references.bib", "lines"], ownerId) - .should.equal true - - it 'should insert universe.jpg', -> - @ProjectEntityUpdateHandler.addFile - .calledWith( - project_id, rootFolderId, "universe.jpg", - Path.resolve(__dirname + "/../../../../app/templates/project_files/universe.jpg"), - null, - ownerId - ) - .should.equal true - - it 'should build the main.tex template', -> - @handler._buildTemplate - .calledWith("main.tex", ownerId, projectName) - .should.equal true - - it 'should build the references.bib template', -> - @handler._buildTemplate - .calledWith("references.bib", ownerId, projectName) - .should.equal true - - - describe "_buildTemplate", -> - - beforeEach (done)-> - @handler._buildTemplate "main.tex", @user_id, projectName, (err, templateLines)=> - @template = templateLines.reduce (singleLine, line)-> "#{singleLine}\n#{line}" - done() - - it "should insert the project name into the template", (done)-> - @template.indexOf(projectName).should.not.equal -1 - done() - - it "should insert the users name into the template", (done)-> - @template.indexOf(@user.first_name).should.not.equal -1 - @template.indexOf(@user.last_name).should.not.equal -1 - done() - - it "should not have undefined in the template", (done)-> - @template.indexOf("undefined").should.equal -1 - done() - - it "should not have any underscore brackets in the output", (done)-> - @template.indexOf("{{").should.equal -1 - @template.indexOf("<%=").should.equal -1 - done() - - it "should put the year in", (done)-> - @template.indexOf(new Date().getUTCFullYear()).should.not.equal -1 - done() - - describe "_createRootDoc", -> - beforeEach (done)-> - @project = new @ProjectModel() - - @handler._createRootDoc @project, ownerId, ["line 1", "line 2"], done - - it 'should insert main.tex', -> - @ProjectEntityUpdateHandler.addDoc - .calledWith(project_id, rootFolderId, "main.tex", ["line 1", "line 2"], ownerId) - .should.equal true - - it 'should set the main doc id', -> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(project_id, docId).should.equal true \ No newline at end of file diff --git a/services/web/test/unit/coffee/Project/ProjectDeleterTests.coffee b/services/web/test/unit/coffee/Project/ProjectDeleterTests.coffee deleted file mode 100644 index b82f65714e..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectDeleterTests.coffee +++ /dev/null @@ -1,163 +0,0 @@ -should = require('chai').should() -modulePath = "../../../../app/js/Features/Project/ProjectDeleter" -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') - -describe 'ProjectDeleter', -> - - beforeEach -> - @project_id = "12312" - @project = - _id: @project_id - rootFolder:[] - collaberator_refs:["collab1", "collab2"] - readOnly_refs:["readOnly1", "readOnly2"] - owner_ref:"owner ref here" - remove: sinon.stub().callsArg(0) - - @user = - _id:"588f3ddae8ebc1bac07c9fa4" - first_name: "bjkdsjfk" - features: {} - - @Project = - update: sinon.stub().callsArgWith(3) - remove: sinon.stub().callsArgWith(1) - findOne: sinon.stub().callsArgWith(1, null, @project) - find: sinon.stub().callsArgWith(1, null, [@project]) - applyToAllFilesRecursivly: sinon.stub() - @DeletedProject = class DeletedProject - constructor: -> - save: sinon.stub().callsArgWith(0) - @documentUpdaterHandler = - flushProjectToMongoAndDelete:sinon.stub().callsArgWith(1) - @editorController = notifyUsersProjectHasBeenDeletedOrRenamed : sinon.stub().callsArgWith(1) - @TagsHandler = - removeProjectFromAllTags: sinon.stub().callsArgWith(2) - @CollaboratorsHandler = - removeUserFromAllProjets: sinon.stub().yields() - getMemberIds: sinon.stub().withArgs(@project_id).yields(null, ["member-id-1", "member-id-2"]) - @deleter = SandboxedModule.require modulePath, requires: - "../Editor/EditorController": @editorController - '../../models/Project':{Project:@Project} - '../../models/DeletedProject':{DeletedProject:@DeletedProject} - '../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler - "../Tags/TagsHandler":@TagsHandler - "../FileStore/FileStoreHandler": @FileStoreHandler = {} - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler - 'logger-sharelatex': - log:-> - - describe "mark as deleted by external source", -> - project_id = 1234 - it 'should update the project with the flag set to true', (done)-> - @deleter.markAsDeletedByExternalSource project_id, => - conditions = {_id:project_id} - update = {deletedByExternalDataSource:true} - @Project.update.calledWith(conditions, update).should.equal true - done() - - it 'should tell the editor controler so users are notified', (done)-> - @deleter.markAsDeletedByExternalSource project_id, => - @editorController.notifyUsersProjectHasBeenDeletedOrRenamed.calledWith(project_id).should.equal true - done() - - describe "unmarkAsDeletedByExternalSource", -> - beforeEach -> - @Project.update = sinon.stub().callsArg(3) - @callback = sinon.stub() - @project = { - _id: @project_id - } - @deleter.unmarkAsDeletedByExternalSource @project_id, @callback - - it "should remove the flag from the project", -> - @Project.update - .calledWith({_id: @project_id}, {deletedByExternalDataSource:false}) - .should.equal true - - describe "deleteUsersProjects", -> - beforeEach -> - @deleter.deleteProject = sinon.stub().callsArg(1) - - it "should find all the projects owned by the user_id", (done)-> - @deleter.deleteUsersProjects @user._id, => - sinon.assert.calledWith(@Project.find, owner_ref: @user._id) - done() - - it "should call deleteProject on the found projects", (done)-> - @deleter.deleteUsersProjects @user._id, => - sinon.assert.calledWith(@deleter.deleteProject, @project._id) - done() - - it "should call deleteProject once for each project", (done)-> - @Project.find.callsArgWith(1, null, [ - {_id: 'potato'}, {_id: 'wombat'} - ]) - @deleter.deleteUsersProjects @user._id, => - sinon.assert.calledTwice(@deleter.deleteProject) - sinon.assert.calledWith(@deleter.deleteProject, 'wombat') - sinon.assert.calledWith(@deleter.deleteProject, 'potato') - done() - - it "should remove all the projects the user is a collaborator of", (done)-> - @deleter.deleteUsersProjects @user._id, => - @CollaboratorsHandler.removeUserFromAllProjets.calledWith(@user._id).should.equal true - done() - - describe "deleteProject", -> - beforeEach () -> - @project_id = "mock-project-id-123" - @ip = "192.170.18.1" - - it "should save a DeletedProject with additional deleterData", (done) -> - @deleter.deleteProject @project_id, {deleterUser: @user, ipAddress: @ip}, (err, deletedProject) => - @DeletedProject::save.called.should.equal true - deletedProject.deleterData.deleterIpAddress.should.equal(@ip) - deletedProject.deleterData.deleterId.should.equal(@user._id) - done() - - it "should flushProjectToMongoAndDelete in doc updater", (done)-> - @deleter.deleteProject @project_id, {deleterUser: @user, ipAddress: @ip}, => - @documentUpdaterHandler.flushProjectToMongoAndDelete.calledWith(@project_id).should.equal true - done() - - it "should removeProjectFromAllTags", (done)-> - @deleter.deleteProject @project_id, => - @TagsHandler.removeProjectFromAllTags.calledWith("member-id-1", @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith("member-id-2", @project_id).should.equal true - done() - - it "should remove the project from Mongo", (done) -> - @deleter.deleteProject @project_id, => - @Project.remove.calledWith({ - _id: @project_id - }).should.equal true - done() - - describe "archiveProject", -> - beforeEach -> - @Project.update.callsArgWith(2) - - it "should update the project", (done)-> - @deleter.archiveProject @project_id, => - @Project.update.calledWith({ - _id:@project_id - }, { - $set: { archived: true } - }).should.equal true - done() - - describe "restoreProject", -> - beforeEach -> - @Project.update.callsArgWith(2) - - it "should unset the archive attribute", (done)-> - @deleter.restoreProject @project_id, => - @Project.update.calledWith({ - _id: @project_id - }, { - $unset: { archived: true } - }).should.equal true - done() - diff --git a/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee deleted file mode 100644 index 41e352eab6..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee +++ /dev/null @@ -1,424 +0,0 @@ -should = require('chai').should() -modulePath = "../../../../app/js/Features/Project/ProjectDetailsHandler" -Errors = require "../../../../app/js/Features/Errors/Errors" -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -assert = require("chai").assert -expect = require("chai").expect -require('chai').should() - -describe 'ProjectDetailsHandler', -> - - beforeEach -> - @project_id = "321l3j1kjkjl" - @user_id = "user-id-123" - @project = - name: "project" - description: "this is a great project" - something:"should not exist" - compiler: "latexxxxxx" - owner_ref: @user_id - @user = - features: "mock-features" - @ProjectGetter = - getProjectWithoutDocLines: sinon.stub().callsArgWith(1, null, @project) - getProject: sinon.stub().callsArgWith(2, null, @project) - @ProjectModel = - update: sinon.stub() - findOne: sinon.stub() - @UserGetter = - getUser: sinon.stub().callsArgWith(1, null, @user) - @tpdsUpdateSender = - moveEntity:sinon.stub().callsArgWith 1 - @ProjectEntityHandler = - flushProjectToThirdPartyDataStore: sinon.stub().callsArg(1) - @CollaboratorsHandler = - removeUserFromProject: sinon.stub().callsArg(2) - @handler = SandboxedModule.require modulePath, requires: - "./ProjectGetter":@ProjectGetter - '../../models/Project': Project:@ProjectModel - "../User/UserGetter": @UserGetter - '../ThirdPartyDataStore/TpdsUpdateSender':@tpdsUpdateSender - "./ProjectEntityHandler": @ProjectEntityHandler - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler - 'logger-sharelatex': - log:-> - err:-> - './ProjectTokenGenerator': @ProjectTokenGenerator = {} - 'settings-sharelatex': @settings = - defaultFeatures: 'default-features' - - describe "getDetails", -> - - it "should find the project and owner", (done)-> - @handler.getDetails @project_id, (err, details)=> - details.name.should.equal @project.name - details.description.should.equal @project.description - details.compiler.should.equal @project.compiler - details.features.should.equal @user.features - assert.equal(details.something, undefined) - done() - - it "should find overleaf metadata if it exists", (done)-> - @project.overleaf = { id: 'id' } - @handler.getDetails @project_id, (err, details)=> - details.overleaf.should.equal @project.overleaf - assert.equal(details.something, undefined) - done() - - it "should return an error for a non-existent project", (done)-> - @ProjectGetter.getProject.callsArg(2, null, null) - err = new Errors.NotFoundError("project not found") - @handler.getDetails "0123456789012345678901234", (error, details) => - err.should.eql error - done() - - it 'should return the default features if no owner found', (done) -> - @UserGetter.getUser.callsArgWith(1, null, null) - @handler.getDetails @project_id, (err, details)=> - details.features.should.equal @settings.defaultFeatures - done() - - it "should return the error", (done)-> - error = "some error" - @ProjectGetter.getProject.callsArgWith(2, error) - @handler.getDetails @project_id, (err)=> - err.should.equal error - done() - - describe "transferOwnership", -> - beforeEach -> - @handler.generateUniqueName = sinon.stub().callsArgWith(2, null, 'teapot') - @ProjectModel.update.callsArgWith(2) - - it "should return a not found error if the project can't be found", (done) -> - @ProjectGetter.getProject.callsArgWith(2) - @handler.transferOwnership 'abc', '123', (err) -> - err.should.exist - err.name.should.equal "NotFoundError" - done() - - it "should return a not found error if the user can't be found", (done) -> - @ProjectGetter.getProject.callsArgWith(2) - @handler.transferOwnership 'abc', '123', (err) -> - err.should.exist - err.name.should.equal "NotFoundError" - done() - - it "should return an error if user cannot be removed as collaborator ", (done) -> - errorMessage = "user-cannot-be-removed" - @CollaboratorsHandler.removeUserFromProject.callsArgWith(2, errorMessage) - @handler.transferOwnership 'abc', '123', (err) -> - err.should.exist - err.should.equal errorMessage - done() - - it "should transfer ownership of the project", (done) -> - @handler.transferOwnership 'abc', '123', () => - sinon.assert.calledWith(@ProjectModel.update, {_id: 'abc'}, sinon.match({$set: {name: 'teapot'}})) - done() - - it "should remove the user from the project's collaborators", (done) -> - @handler.transferOwnership 'abc', '123', () => - sinon.assert.calledWith(@CollaboratorsHandler.removeUserFromProject, 'abc', '123') - done() - - it "should flush the project to tpds", (done) -> - @handler.transferOwnership 'abc', '123', () => - sinon.assert.calledWith(@ProjectEntityHandler.flushProjectToThirdPartyDataStore, 'abc') - done() - - it "should generate a unique name for the project", (done) -> - @handler.transferOwnership 'abc', '123', () => - sinon.assert.calledWith(@handler.generateUniqueName, '123', @project.name) - done() - - it "should append the supplied suffix to the project name, if passed", (done) -> - @handler.transferOwnership 'abc', '123', ' wombat', () => - sinon.assert.calledWith(@handler.generateUniqueName, '123', "#{@project.name} wombat") - done() - - describe "getProjectDescription", -> - - it "should make a call to mongo just for the description", (done)-> - @ProjectGetter.getProject.callsArgWith(2) - @handler.getProjectDescription @project_id, (err, description)=> - @ProjectGetter.getProject - .calledWith(@project_id, description: true) - .should.equal true - done() - - it "should return what the mongo call returns", (done)-> - err = "error" - description = "cool project" - @ProjectGetter.getProject.callsArgWith(2, err, {description:description}) - @handler.getProjectDescription @project_id, (returnedErr, returnedDescription)=> - err.should.equal returnedErr - description.should.equal returnedDescription - done() - - describe "setProjectDescription", -> - - beforeEach -> - @description = "updated teh description" - - it "should update the project detials", (done)-> - @ProjectModel.update.callsArgWith(2) - @handler.setProjectDescription @project_id, @description, => - @ProjectModel.update.calledWith({_id:@project_id}, {description:@description}).should.equal true - done() - - describe "renameProject", -> - beforeEach -> - @handler.validateProjectName = sinon.stub().yields() - @ProjectModel.update.callsArgWith(2) - @newName = "new name here" - - it "should update the project with the new name", (done)-> - newName = "new name here" - @handler.renameProject @project_id, @newName, => - @ProjectModel.update.calledWith({_id: @project_id}, {name: @newName}).should.equal true - done() - - it "should tell the tpdsUpdateSender", (done)-> - @handler.renameProject @project_id, @newName, => - @tpdsUpdateSender.moveEntity.calledWith({project_id:@project_id, project_name:@project.name, newProjectName:@newName}).should.equal true - done() - - it "should not do anything with an invalid name", (done) -> - @handler.validateProjectName = sinon.stub().yields(new Error("invalid name")) - @handler.renameProject @project_id, @newName, => - @tpdsUpdateSender.moveEntity.called.should.equal false - @ProjectModel.update.called.should.equal false - done() - - describe "validateProjectName", -> - - it "should reject undefined names", (done) -> - @handler.validateProjectName undefined, (error) -> - expect(error).to.exist - done() - - it "should reject empty names", (done) -> - @handler.validateProjectName "", (error) -> - expect(error).to.exist - done() - - it "should reject names with /s", (done) -> - @handler.validateProjectName "foo/bar", (error) -> - expect(error).to.exist - done() - - it "should reject names with \\s", (done) -> - @handler.validateProjectName "foo\\bar", (error) -> - expect(error).to.exist - done() - - it "should reject long names", (done) -> - @handler.validateProjectName new Array(1000).join("a"), (error) -> - expect(error).to.exist - done() - - it "should accept normal names", (done) -> - @handler.validateProjectName "foobar", (error) -> - expect(error).to.not.exist - done() - - describe "ensureProjectNameIsUnique", -> - beforeEach -> - @result = { - owned: [{_id: 1, name:"name"}, {_id: 2, name: "name1"}, {_id: 3, name: "name11"}, {_id: 100, name: "numeric"}] - readAndWrite: [{_id: 4, name:"name2"}, {_id: 5, name:"name22"}] - readOnly: [{_id:6, name:"name3"}, {_id:7, name: "name33"}] - tokenReadAndWrite: [{_id:8, name:"name4"}, {_id:9, name:"name44"}] - tokenReadOnly: [{_id:10, name:"name5"}, {_id:11, name:"name55"}, {_id:12, name:"x".repeat(15)}] - } - for i in [1..20].concat([30..40]) - @result.owned.push {_id: 100 + i, name: "numeric (#{i})"} - @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, @result) - - it "should leave a unique name unchanged", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "unique-name", ["-test-suffix"], (error, name, changed) -> - expect(name).to.equal "unique-name" - expect(changed).to.equal false - done() - - it "should append a suffix to an existing name", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "name1", ["-test-suffix"], (error, name, changed) -> - expect(name).to.equal "name1-test-suffix" - expect(changed).to.equal true - done() - - it "should fallback to a second suffix when needed", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "name1", ["1", "-test-suffix"], (error, name, changed) -> - expect(name).to.equal "name1-test-suffix" - expect(changed).to.equal true - done() - - it "should truncate the name when append a suffix if the result is too long", (done) -> - @handler.MAX_PROJECT_NAME_LENGTH = 20 - @handler.ensureProjectNameIsUnique @user_id, "x".repeat(15), ["-test-suffix"], (error, name, changed) -> - expect(name).to.equal "x".repeat(8) + "-test-suffix" - expect(changed).to.equal true - done() - - it "should use a numeric index if no suffix is supplied", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "name1", [], (error, name, changed) -> - expect(name).to.equal "name1 (1)" - expect(changed).to.equal true - done() - - it "should use a numeric index if all suffixes are exhausted", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "name", ["1", "11"], (error, name, changed) -> - expect(name).to.equal "name (1)" - expect(changed).to.equal true - done() - - it "should find the next lowest available numeric index for the base name", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "numeric", [], (error, name, changed) -> - expect(name).to.equal "numeric (21)" - expect(changed).to.equal true - done() - - it "should find the next available numeric index when a numeric index is already present", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "numeric (5)", [], (error, name, changed) -> - expect(name).to.equal "numeric (21)" - expect(changed).to.equal true - done() - - it "should not find a numeric index lower than the one already present", (done) -> - @handler.ensureProjectNameIsUnique @user_id, "numeric (31)", [], (error, name, changed) -> - expect(name).to.equal "numeric (41)" - expect(changed).to.equal true - done() - - describe "fixProjectName", -> - - it "should change empty names to Untitled", () -> - expect(@handler.fixProjectName "").to.equal "Untitled" - - it "should replace / with -", () -> - expect(@handler.fixProjectName "foo/bar").to.equal "foo-bar" - - it "should replace \\ with ''", () -> - expect(@handler.fixProjectName "foo \\ bar").to.equal "foo bar" - - it "should truncate long names", () -> - expect(@handler.fixProjectName new Array(1000).join("a")).to.equal "a".repeat(150) - - it "should accept normal names", () -> - expect(@handler.fixProjectName "foobar").to.equal "foobar" - - - describe "setPublicAccessLevel", -> - beforeEach -> - @ProjectModel.update.callsArgWith(2) - @accessLevel = "readOnly" - - it "should update the project with the new level", (done)-> - @handler.setPublicAccessLevel @project_id, @accessLevel, => - @ProjectModel.update.calledWith({_id: @project_id}, {publicAccesLevel: @accessLevel}).should.equal true - done() - - it 'should not produce an error', (done) -> - @handler.setPublicAccessLevel @project_id, @accessLevel, (err) => - expect(err).to.not.exist - done() - - describe 'when update produces an error', -> - beforeEach -> - @ProjectModel.update.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @handler.setPublicAccessLevel @project_id, @accessLevel, (err) => - expect(err).to.exist - expect(err).to.be.instanceof Error - done() - - describe "ensureTokensArePresent", -> - beforeEach -> - - describe 'when the project has tokens', -> - beforeEach -> - @project = - _id: @project_id - tokens: - readOnly: 'aaa' - readAndWrite: '42bbb' - readAndWritePrefix: '42' - @ProjectGetter.getProject = sinon.stub() - .callsArgWith(2, null, @project) - @ProjectModel.update = sinon.stub() - - it 'should get the project', (done) -> - @handler.ensureTokensArePresent @project_id, (err, tokens) => - expect(@ProjectGetter.getProject.callCount).to.equal 1 - expect(@ProjectGetter.getProject.calledWith(@project_id, {tokens: 1})) - .to.equal true - done() - - it 'should not update the project with new tokens', (done) -> - @handler.ensureTokensArePresent @project_id, (err, tokens) => - expect(@ProjectModel.update.callCount).to.equal 0 - done() - - it 'should produce the tokens without error', (done) -> - @handler.ensureTokensArePresent @project_id, (err, tokens) => - expect(err).to.not.exist - expect(tokens).to.deep.equal @project.tokens - done() - - describe 'when tokens are missing', -> - beforeEach -> - @project = - _id: @project_id - @ProjectGetter.getProject = sinon.stub() - .callsArgWith(2, null, @project) - @readOnlyToken = 'abc' - @readAndWriteToken = '42def' - @readAndWriteTokenPrefix = '42' - @ProjectTokenGenerator.generateUniqueReadOnlyToken = sinon.stub().callsArgWith(0, null, @readOnlyToken) - @ProjectTokenGenerator.readAndWriteToken = sinon.stub().returns({ - token: @readAndWriteToken - numericPrefix: @readAndWriteTokenPrefix - }) - @ProjectModel.update = sinon.stub() - .callsArgWith(2, null) - - it 'should get the project', (done) -> - @handler.ensureTokensArePresent @project_id, (err, tokens) => - expect(@ProjectGetter.getProject.callCount).to.equal 1 - expect(@ProjectGetter.getProject.calledWith(@project_id, {tokens: 1})) - .to.equal true - done() - - it 'should update the project with new tokens', (done) -> - @handler.ensureTokensArePresent @project_id, (err, tokens) => - expect(@ProjectTokenGenerator.generateUniqueReadOnlyToken.callCount) - .to.equal 1 - expect(@ProjectTokenGenerator.readAndWriteToken.callCount) - .to.equal 1 - expect(@ProjectModel.update.callCount).to.equal 1 - expect(@ProjectModel.update.calledWith( - {_id: @project_id}, - { - $set: { - tokens: { - readOnly: @readOnlyToken, - readAndWrite: @readAndWriteToken, - readAndWritePrefix: @readAndWriteTokenPrefix - } - } - } - )).to.equal true - done() - - it 'should produce the tokens without error', (done) -> - @handler.ensureTokensArePresent @project_id, (err, tokens) => - expect(err).to.not.exist - expect(tokens).to.deep.equal { - readOnly: @readOnlyToken, - readAndWrite: @readAndWriteToken, - readAndWritePrefix: @readAndWriteTokenPrefix - } - done() diff --git a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee deleted file mode 100644 index 5657dab37b..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee +++ /dev/null @@ -1,200 +0,0 @@ -sinon = require('sinon') -chai = require('chai').should() -modulePath = "../../../../app/js/Features/Project/ProjectDuplicator.js" -SandboxedModule = require('sandboxed-module') - -describe 'ProjectDuplicator', -> - - beforeEach -> - @level2folder = - name: "level2folderName" - _id:"level2folderId" - docs:[@doc2 = {_id: "doc2_id", name:"level2folderDocName"}, undefined] - folders:[] - fileRefs:[{name:"file2", _id:"file2"}] - @level1folder = - name:"level1folder" - _id:"level1folderId" - docs:[@doc1 = {_id: "doc1_id", name:"level1folderDocName"}] - folders:[@level2folder] - fileRefs:[{name:"file1", _id:"file1"}, null] # the null is intentional to test null docs/files - @rootFolder = - name:"rootFolder" - _id:"rootFolderId" - docs:[@doc0 = {_id: "doc0_id", name:"rootDocHere"}] - folders:[@level1folder, {}] - fileRefs:[{name:"file0", _id:"file0"}] - @project = - _id: @old_project_id = "this_is_the_old_project_id" - rootDoc_id: "rootDoc_id" - rootFolder:[@rootFolder] - compiler: "this_is_a_Compiler" - - @docContents = [{ - _id: @doc0._id - lines: @doc0_lines = ["zero"] - }, { - _id: @doc1._id - lines: @doc1_lines = ["one"] - }, { - _id: @doc2._id - lines: @doc2_lines = ["two"] - }] - @DocstoreManager = - getAllDocs: sinon.stub().callsArgWith(1, null, @docContents) - - @owner = {_id:"this_is_the_owner"} - @stubbedNewProject = - _id: @new_project_id = "new_project_id" - readOnly_refs:[] - collaberator_refs:[] - rootFolder:[ - {_id:"new_root_folder_id"} - ] - @foundRootDoc = {_id:"rootDocId", name:"rootDocHere"} - - @creationHandler = - createBlankProject : sinon.stub().callsArgWith(2, null, @stubbedNewProject) - - @newFolder = {_id: "newFolderId"} - - @locator = - findRootDoc : sinon.stub().callsArgWith(1, null, @foundRootDoc, {}) - - @projectOptionsHandler = - setCompiler : sinon.stub().callsArg(2) - @ProjectEntityUpdateHandler = - addDoc: sinon.stub().callsArgWith(5, null, {name:"somDoc"}) - copyFileFromExistingProjectWithProject: sinon.stub() - setRootDoc: sinon.stub() - addFolder: sinon.stub().callsArgWith(3, null, @newFolder) - - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject.withArgs(sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any, "BROKEN-FILE", sinon.match.any, sinon.match.any).callsArgWith(6, new Error("failed")) - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject.withArgs(sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.object, sinon.match.any).callsArg(6) - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject.withArgs(sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any, null, sinon.match.any).callsArg(6) - - @DocumentUpdaterHandler = - flushProjectToMongo: sinon.stub().callsArg(1) - - @Project = - findById: sinon.stub().callsArgWith(1, null, @project) - - @ProjectGetter = - getProject: sinon.stub() - - @ProjectGetter.getProject.withArgs(@old_project_id, sinon.match.any).callsArgWith(2, null, @project) - @ProjectGetter.getProject.withArgs(@new_project_id, sinon.match.any).callsArgWith(2, null, @stubbedNewProject) - - @ProjectDeleter = - deleteProject: sinon.stub().callsArgWith(1,null) - - @duplicator = SandboxedModule.require modulePath, requires: - '../../models/Project':{Project:@Project} - "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler - './ProjectCreationHandler': @creationHandler - './ProjectEntityUpdateHandler': @ProjectEntityUpdateHandler - './ProjectLocator': @locator - './ProjectDeleter': @ProjectDeleter - './ProjectOptionsHandler': @projectOptionsHandler - "../Docstore/DocstoreManager": @DocstoreManager - "./ProjectGetter":@ProjectGetter - 'logger-sharelatex':{ - log:-> - err:-> - } - - describe "when the copy succeeds", -> - - it "should look up the original project", (done) -> - newProjectName = "someProj" - @duplicator.duplicate @owner, @old_project_id, newProjectName, (err, newProject)=> - @ProjectGetter.getProject.calledWith(@old_project_id).should.equal true - done() - - it "should flush the original project to mongo", (done) -> - newProjectName = "someProj" - @duplicator.duplicate @owner, @old_project_id, newProjectName, (err, newProject)=> - @DocumentUpdaterHandler.flushProjectToMongo.calledWith(@old_project_id).should.equal true - done() - - it 'should create a blank project', (done)-> - newProjectName = "someProj" - @duplicator.duplicate @owner, @old_project_id, newProjectName, (err, newProject)=> - newProject._id.should.equal @stubbedNewProject._id - @creationHandler.createBlankProject.calledWith(@owner._id, newProjectName).should.equal true - done() - - it 'should use the same compiler', (done)-> - @ProjectEntityUpdateHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true - done() - - it 'should use the same root doc', (done)-> - @ProjectEntityUpdateHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true - done() - - it 'should not copy the collaberators or read only refs', (done)-> - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - newProject.collaberator_refs.length.should.equal 0 - newProject.readOnly_refs.length.should.equal 0 - done() - - it 'should copy all the folders', (done)-> - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @ProjectEntityUpdateHandler.addFolder.calledWith(@new_project_id, @stubbedNewProject.rootFolder[0]._id, @level1folder.name).should.equal true - @ProjectEntityUpdateHandler.addFolder.calledWith(@new_project_id, @newFolder._id, @level2folder.name).should.equal true - @ProjectEntityUpdateHandler.addFolder.callCount.should.equal 2 - done() - - it 'should copy all the docs', (done)-> - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true - @ProjectEntityUpdateHandler.addDoc - .calledWith(@new_project_id, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id) - .should.equal true - @ProjectEntityUpdateHandler.addDoc - .calledWith(@new_project_id, @newFolder._id, @doc1.name, @doc1_lines, @owner._id) - .should.equal true - @ProjectEntityUpdateHandler.addDoc - .calledWith(@new_project_id, @newFolder._id, @doc2.name, @doc2_lines, @owner._id) - .should.equal true - done() - - it 'should copy all the files', (done)-> - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject - .calledWith(@stubbedNewProject._id, @stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @project._id, @rootFolder.fileRefs[0], @owner._id) - .should.equal true - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject - .calledWith(@stubbedNewProject._id, @stubbedNewProject, @newFolder._id, @project._id, @level1folder.fileRefs[0], @owner._id) - .should.equal true - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject - .calledWith(@stubbedNewProject._id, @stubbedNewProject, @newFolder._id, @project._id, @level2folder.fileRefs[0], @owner._id) - .should.equal true - done() - - describe 'when there is an error', -> - beforeEach -> - @rootFolder.fileRefs = [{name:"file0", _id:"file0"}, "BROKEN-FILE", {name:"file1", _id:"file1"}, {name:"file2", _id:"file2"}] - - it 'should delete the broken cloned project', (done) -> - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @ProjectDeleter.deleteProject - .calledWith(@stubbedNewProject._id) - .should.equal true - done() - - it 'should not delete the original project', (done) -> - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @ProjectDeleter.deleteProject - .calledWith(@old_project_id) - .should.equal false - done() - - it 'should return an error', (done) -> - @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - err.should.not.equal null - done() \ No newline at end of file diff --git a/services/web/test/unit/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEditorHandlerTests.coffee deleted file mode 100644 index 178df1d70d..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectEditorHandlerTests.coffee +++ /dev/null @@ -1,223 +0,0 @@ -chai = require('chai') -expect = chai.expect -should = chai.should() - -modulePath = "../../../../app/js/Features/Project/ProjectEditorHandler" -SandboxedModule = require('sandboxed-module') - -describe "ProjectEditorHandler", -> - beforeEach -> - @project = - _id : "project-id" - name : "Project Name" - rootDoc_id : "file-id" - publicAccesLevel : "private" - deletedByExternalDataSource: false - rootFolder : [{ - _id : "root-folder-id" - name : "" - docs : [] - fileRefs : [] - folders : [{ - _id : "sub-folder-id" - name : "folder" - docs : [{ - _id : "doc-id" - name : "main.tex" - lines : @lines = [ - "line 1" - "line 2" - "line 3" - ] - }] - fileRefs : [{ - _id : "file-id" - name : "image.png" - created : @created = new Date() - size : 1234 - }] - folders : [] - }] - }] - deletedDocs: [{ - _id: "deleted-doc-id" - name: "main.tex" - deletedAt: @deletedAt = new Date("2017-01-01") - }] - @members = [{ - user: @owner = { - _id: "owner-id" - first_name : "Owner" - last_name : "ShareLaTeX" - email : "owner@sharelatex.com" - }, - privilegeLevel: "owner" - },{ - user: { - _id: "read-only-id" - first_name : "Read" - last_name : "Only" - email : "read-only@sharelatex.com" - }, - privilegeLevel: "readOnly" - },{ - user: { - _id: "read-write-id" - first_name : "Read" - last_name : "Write" - email : "read-write@sharelatex.com" - }, - privilegeLevel: "readAndWrite" - }] - @invites = [ - {_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id} - {_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id} - ] - @handler = SandboxedModule.require modulePath - - describe "buildProjectModelView", -> - describe "with owner and members included", -> - beforeEach -> - @result = @handler.buildProjectModelView @project, @members, @invites - - it "should include the id", -> - should.exist @result._id - @result._id.should.equal "project-id" - - it "should include the name", -> - should.exist @result.name - @result.name.should.equal "Project Name" - - it "should include the root doc id", -> - should.exist @result.rootDoc_id - @result.rootDoc_id.should.equal "file-id" - - it "should include the public access level", -> - should.exist @result.publicAccesLevel - @result.publicAccesLevel.should.equal "private" - - it "should include the owner", -> - should.exist @result.owner - @result.owner._id.should.equal "owner-id" - @result.owner.email.should.equal "owner@sharelatex.com" - @result.owner.first_name.should.equal "Owner" - @result.owner.last_name.should.equal "ShareLaTeX" - @result.owner.privileges.should.equal "owner" - - it "should include the deletedDocs", -> - should.exist @result.deletedDocs - @result.deletedDocs.should.equal @project.deletedDocs - - it "should gather readOnly_refs and collaberators_refs into a list of members", -> - findMember = (id) => - for member in @result.members - return member if member._id == id - return null - - @result.members.length.should.equal 2 - - should.exist findMember("read-only-id") - findMember("read-only-id").privileges.should.equal "readOnly" - findMember("read-only-id").first_name.should.equal "Read" - findMember("read-only-id").last_name.should.equal "Only" - findMember("read-only-id").email.should.equal "read-only@sharelatex.com" - - should.exist findMember("read-write-id") - findMember("read-write-id").privileges.should.equal "readAndWrite" - findMember("read-write-id").first_name.should.equal "Read" - findMember("read-write-id").last_name.should.equal "Write" - findMember("read-write-id").email.should.equal "read-write@sharelatex.com" - - it "should include folders in the project", -> - @result.rootFolder[0]._id.should.equal "root-folder-id" - @result.rootFolder[0].name.should.equal "" - - @result.rootFolder[0].folders[0]._id.should.equal "sub-folder-id" - @result.rootFolder[0].folders[0].name.should.equal "folder" - - it "should not duplicate folder contents", -> - @result.rootFolder[0].docs.length.should.equal 0 - @result.rootFolder[0].fileRefs.length.should.equal 0 - - it "should include files in the project", -> - @result.rootFolder[0].folders[0].fileRefs[0]._id.should.equal "file-id" - @result.rootFolder[0].folders[0].fileRefs[0].name.should.equal "image.png" - @result.rootFolder[0].folders[0].fileRefs[0].created.should.equal @created - should.not.exist @result.rootFolder[0].folders[0].fileRefs[0].size - - it "should include docs in the project but not the lines", -> - @result.rootFolder[0].folders[0].docs[0]._id.should.equal "doc-id" - @result.rootFolder[0].folders[0].docs[0].name.should.equal "main.tex" - should.not.exist @result.rootFolder[0].folders[0].docs[0].lines - - it 'should include invites', -> - should.exist @result.invites - @result.invites.should.deep.equal @invites - - describe "deletedByExternalDataSource", -> - - it "should set the deletedByExternalDataSource flag to false when it is not there", -> - delete @project.deletedByExternalDataSource - result = @handler.buildProjectModelView @project, @members, [], [] - result.deletedByExternalDataSource.should.equal false - - it "should set the deletedByExternalDataSource flag to false when it is false", -> - result = @handler.buildProjectModelView @project, @members, [], [] - result.deletedByExternalDataSource.should.equal false - - it "should set the deletedByExternalDataSource flag to true when it is true", -> - @project.deletedByExternalDataSource = true - result = @handler.buildProjectModelView @project, @members, [], [] - result.deletedByExternalDataSource.should.equal true - - describe "features", -> - beforeEach -> - @owner.features = - versioning: true - collaborators: 3 - compileGroup:"priority" - compileTimeout: 96 - @result = @handler.buildProjectModelView @project, @members, [], [] - - it "should copy the owner features to the project", -> - @result.features.versioning.should.equal @owner.features.versioning - @result.features.collaborators.should.equal @owner.features.collaborators - @result.features.compileGroup.should.equal @owner.features.compileGroup - @result.features.compileTimeout.should.equal @owner.features.compileTimeout - - describe 'buildOwnerAndMembersViews', -> - beforeEach -> - @owner.features = - versioning: true - collaborators: 3 - compileGroup:"priority" - compileTimeout: 22 - @result = @handler.buildOwnerAndMembersViews @members - - it 'should produce an object with the right keys', -> - expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members'] - - it 'should separate the owner from the members', -> - @result.members.length.should.equal(@members.length-1) - expect(@result.owner._id).to.equal @owner._id - expect(@result.owner.email).to.equal @owner.email - expect(@result.members.filter((m) => m._id == @owner._id).length).to.equal 0 - - it 'should extract the ownerFeatures from the owner object', -> - expect(@result.ownerFeatures).to.deep.equal @owner.features - - describe 'when there is no owner', -> - beforeEach -> - # remove the owner from members list - @membersWithoutOwner = @members.filter((m) => m.user._id != @owner._id) - @result = @handler.buildOwnerAndMembersViews @membersWithoutOwner - - it 'should produce an object with the right keys', -> - expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members'] - - it 'should not separate out an owner', -> - @result.members.length.should.equal @membersWithoutOwner.length - expect(@result.owner).to.equal null - - it 'should not extract the ownerFeatures from the owner object', -> - expect(@result.ownerFeatures).to.equal null diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee deleted file mode 100644 index 77a4816f4c..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee +++ /dev/null @@ -1,252 +0,0 @@ -chai = require('chai') -assert = require('chai').assert -should = chai.should() -expect = chai.expect -sinon = require 'sinon' -modulePath = "../../../../app/js/Features/Project/ProjectEntityHandler" -SandboxedModule = require('sandboxed-module') -ObjectId = require("mongoose").Types.ObjectId -Errors = require "../../../../app/js/Features/Errors/Errors" - -describe 'ProjectEntityHandler', -> - project_id = '4eecb1c1bffa66588e0000a1' - doc_id = '4eecb1c1bffa66588e0000a2' - folder_id = "4eecaffcbffa66588e000008" - rootFolderId = "4eecaffcbffa66588e000007" - userId = 1234 - - beforeEach -> - @TpdsUpdateSender = - addDoc:sinon.stub().callsArg(1) - addFile:sinon.stub().callsArg(1) - @ProjectModel = class Project - constructor:(options)-> - @._id = project_id - @name = "project_name_here" - @rev = 0 - rootFolder:[@rootFolder] - - @project = new @ProjectModel() - - @ProjectLocator = - findElement : sinon.stub() - @DocumentUpdaterHandler = - updateProjectStructure: sinon.stub().yields() - - @callback = sinon.stub() - - @ProjectEntityHandler = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} - '../Docstore/DocstoreManager': @DocstoreManager = {} - '../../Features/DocumentUpdater/DocumentUpdaterHandler':@DocumentUpdaterHandler - '../../models/Project': Project:@ProjectModel - './ProjectLocator': @ProjectLocator - "./ProjectGetter": @ProjectGetter = {} - '../ThirdPartyDataStore/TpdsUpdateSender':@TpdsUpdateSender - - describe "getting folders, docs and files", -> - beforeEach -> - @project.rootFolder = [ - docs: [@doc1 = { - name : "doc1" - _id : "doc1_id" - }] - fileRefs: [@file1 = { - rev : 1 - _id : "file1_id" - name : "file1" - }] - folders: [@folder1 = { - name : "folder1" - docs : [@doc2 = { - name : "doc2" - _id : "doc2_id" - }] - fileRefs : [@file2 = { - rev : 2 - name : "file2" - _id : "file2_id" - }] - folders : [] - }] - ] - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields(null, @project) - - describe "getAllDocs", -> - beforeEach -> - @docs = [{ - _id: @doc1._id - lines: @lines1 = ["one"] - rev: @rev1 = 1 - }, { - _id: @doc2._id - lines: @lines2 = ["two"] - rev: @rev2 = 2 - }] - @DocstoreManager.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityHandler.getAllDocs project_id, @callback - - it "should get the doc lines and rev from the docstore", -> - @DocstoreManager.getAllDocs - .calledWith(project_id) - .should.equal true - - it "should call the callback with the docs with the lines and rev included", -> - @callback - .calledWith(null, { - "/doc1": { - _id: @doc1._id - lines: @lines1 - name: @doc1.name - rev: @rev1 - } - "/folder1/doc2": { - _id: @doc2._id - lines: @lines2 - name: @doc2.name - rev: @rev2 - } - }) - .should.equal true - - describe "getAllFiles", -> - beforeEach -> - @callback = sinon.stub() - @ProjectEntityHandler.getAllFiles project_id, @callback - - it "should call the callback with the files", -> - @callback - .calledWith(null, { - "/file1": @file1 - "/folder1/file2": @file2 - }) - .should.equal true - - describe "getAllDocPathsFromProject", -> - beforeEach -> - @docs = [{ - _id: @doc1._id - lines: @lines1 = ["one"] - rev: @rev1 = 1 - }, { - _id: @doc2._id - lines: @lines2 = ["two"] - rev: @rev2 = 2 - }] - @callback = sinon.stub() - @ProjectEntityHandler.getAllDocPathsFromProject @project, @callback - - it "should call the callback with the path for each doc_id", -> - @expected = {} - @expected[@doc1._id] = "/#{@doc1.name}" - @expected[@doc2._id] = "/folder1/#{@doc2.name}" - @callback - .calledWith(null, @expected) - .should.equal true - - describe "_getAllFolders", -> - beforeEach -> - @callback = sinon.stub() - @ProjectEntityHandler._getAllFolders project_id, @callback - - it "should get the project without the docs lines", -> - @ProjectGetter.getProjectWithoutDocLines - .calledWith(project_id) - .should.equal true - - it "should call the callback with the folders", -> - @callback - .calledWith(null, { - "/": @project.rootFolder[0] - "/folder1": @folder1 - }) - .should.equal true - - describe "_getAllFoldersFromProject", -> - beforeEach -> - @callback = sinon.stub() - @ProjectEntityHandler._getAllFoldersFromProject @project, @callback - - it "should call the callback with the folders", -> - @callback - .calledWith(null, { - "/": @project.rootFolder[0] - "/folder1": @folder1 - }) - .should.equal true - - describe "flushProjectToThirdPartyDataStore", -> - beforeEach (done) -> - @project = { - _id: project_id - name: "Mock project name" - } - @DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().yields() - @docs = { - "/doc/one": @doc1 = { _id: "mock-doc-1", lines: ["one"], rev: 5 } - "/doc/two": @doc2 = { _id: "mock-doc-2", lines: ["two"], rev: 6 } - } - @files = { - "/file/one": @file1 = { _id: "mock-file-1", rev: 7 } - "/file/two": @file2 = { _id: "mock-file-2", rev: 8 } - } - @ProjectEntityHandler.getAllDocs = sinon.stub().yields(null, @docs) - @ProjectEntityHandler.getAllFiles = sinon.stub().yields(null, @files) - - @ProjectGetter.getProject = sinon.stub().yields(null, @project) - - @ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, () -> done() - - it "should flush the project from the doc updater", -> - @DocumentUpdaterHandler.flushProjectToMongo.calledWith(project_id).should.equal true - - it "should look up the project in mongo", -> - @ProjectGetter.getProject.calledWith(project_id).should.equal true - - it "should get all the docs in the project", -> - @ProjectEntityHandler.getAllDocs.calledWith(project_id).should.equal true - - it "should get all the files in the project", -> - @ProjectEntityHandler.getAllFiles.calledWith(project_id).should.equal true - - it "should flush each doc to the TPDS", -> - for path, doc of @docs - @TpdsUpdateSender.addDoc - .calledWith({ - project_id: project_id, - doc_id: doc._id - project_name: @project.name - rev: doc.rev - path: path - }) - .should.equal true - - it "should flush each file to the TPDS", -> - for path, file of @files - @TpdsUpdateSender.addFile - .calledWith({ - project_id: project_id, - file_id: file._id - project_name: @project.name - rev: file.rev - path: path - }) - .should.equal true - - describe 'getDoc', -> - beforeEach -> - @lines = ["mock", "doc", "lines"] - @rev = 5 - @version = 42 - @ranges = {"mock": "ranges"} - - @DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev, @version, @ranges) - @ProjectEntityHandler.getDoc project_id, doc_id, @callback - - it "should call the docstore", -> - @DocstoreManager.getDoc - .calledWith(project_id, doc_id) - .should.equal true - - it "should call the callback with the lines, version and rev", -> - @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee deleted file mode 100644 index f8ad980f41..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee +++ /dev/null @@ -1,710 +0,0 @@ -chai = require('chai') -expect = chai.expect -assert = require('chai').assert -should = chai.should() -sinon = require 'sinon' -tk = require("timekeeper") -modulePath = "../../../../app/js/Features/Project/ProjectEntityMongoUpdateHandler" -Errors = require "../../../../app/js/Features/Errors/Errors" -ObjectId = require("mongoose").Types.ObjectId -SandboxedModule = require('sandboxed-module') - -describe 'ProjectEntityMongoUpdateHandler', -> - project_id = '4eecb1c1bffa66588e0000a1' - doc_id = '4eecb1c1bffa66588e0000a2' - file_id = '4eecb1c1bffa66588e0000a3' - folder_id = "4eecaffcbffa66588e000008" - - beforeEach -> - @FolderModel = class Folder - constructor:(options)-> - {@name} = options - @._id = "folder_id" - - @docName = "doc-name" - @fileName = "something.jpg" - @project = _id: project_id, name: 'project name' - - @callback = sinon.stub() - - tk.freeze(Date.now()) - @subject = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} - "settings-sharelatex":@settings = { - maxEntitiesPerProject: 100 - } - "../Cooldown/CooldownManager": @CooldownManager = {} - '../../models/Folder': Folder:@FolderModel - "../../infrastructure/LockManager":@LockManager = - runWithLock: - sinon.spy((namespace, id, runner, callback) -> runner(callback)) - '../../models/Project': Project:@ProjectModel = {} - './ProjectEntityHandler': @ProjectEntityHandler = {} - './ProjectLocator': @ProjectLocator = {} - "./ProjectGetter": @ProjectGetter = - getProjectWithoutLock: sinon.stub().yields(null, @project) - - afterEach -> - tk.reset() - - describe 'addDoc', -> - beforeEach -> - @subject._confirmFolder = sinon.stub().yields(folder_id) - @subject._putElement = sinon.stub() - - @doc = _id: doc_id - @subject.addDoc project_id, folder_id, @doc, @callback - - it 'gets the project', -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it 'checks the folder exists', -> - @subject._confirmFolder - .calledWith(@project, folder_id) - .should.equal true - - it 'puts the element in mongo', -> - @subject._putElement - .calledWith(@project, folder_id, @doc, 'doc', @callback) - .should.equal true - - describe 'addFile', -> - beforeEach -> - @subject._confirmFolder = sinon.stub().yields(folder_id) - @subject._putElement = sinon.stub() - - @file = _id: file_id - @subject.addFile project_id, folder_id, @file, @callback - - it 'gets the project', -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it 'checks the folder exists', -> - @subject._confirmFolder - .calledWith(@project, folder_id) - .should.equal true - - it 'puts the element in mongo', -> - @subject._putElement - .calledWith(@project, folder_id, @file, 'file', @callback) - .should.equal true - - describe 'replaceFileWithNew', -> - beforeEach -> - @file = _id: file_id - @path = mongo: 'file.png' - @newFile = _id: 'new-file-id' - @newFile.linkedFileData = @linkedFileData = {provider: 'url'} - @newProject = "new-project" - @ProjectLocator.findElement = sinon.stub().yields(null, @file, @path) - @ProjectModel.findOneAndUpdate = sinon.stub().yields(null, @newProject) - @ProjectModel.update = sinon.stub().yields() - - @subject.replaceFileWithNew project_id, file_id, @newFile, @callback - - it 'gets the project', -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it 'finds the existing element', -> - @ProjectLocator.findElement - .calledWith({ @project, element_id: file_id, type: 'file' }) - .should.equal true - - it 'inserts a deletedFile reference for the old file', -> - @ProjectModel.update - .calledWith({ _id: project_id }, - { - $push: { - deletedFiles: { - _id: file_id - name: @file.name - linkedFileData: @file.linkedFileData - hash: @file.hash - deletedAt: new Date() - } - } - } - ) - .should.equal true - - it 'increments the project version and sets the rev and created_at', -> - @ProjectModel.findOneAndUpdate - .calledWith( - { _id: project_id }, - { - '$inc': { 'version': 1, 'file.png.rev': 1 } - '$set': { 'file.png._id': @newFile._id, 'file.png.created': new Date(), 'file.png.linkedFileData': @linkedFileData, 'file.png.hash': @hash } - }, - {new: true} - ) - .should.equal true - - it 'calls the callback', -> - @callback.calledWith(null, @file, @project, @path, @newProject).should.equal true - - describe 'mkdirp', -> - beforeEach -> - @parentFolder_id = "1jnjknjk" - @newFolder = {_id:"newFolder_id_here"} - @lastFolder = {_id:"123das", folders:[]} - - @rootFolder = {_id: "rootFolderId" } - @project = _id: project_id, rootFolder: [@rootFolder] - - @ProjectGetter.getProjectWithOnlyFolders = sinon.stub().yields(null, @project) - @ProjectLocator.findElementByPath = -> - sinon.stub @ProjectLocator, "findElementByPath", (options, cb) => - {path} = options - @parentFolder = {_id:"parentFolder_id_here"} - lastFolder = path.substring(path.lastIndexOf("/")) - if lastFolder.indexOf("level1") == -1 - cb "level1 is not the last foler " - else - cb null, @parentFolder - @subject.addFolder = - withoutLock: (project_id, parentFolder_id, folderName, callback) => - callback null, {name:folderName}, @parentFolder_id - - it 'should return the root folder if the path is just a slash', (done)-> - path = "/" - @subject.mkdirp project_id, path, {}, (err, folders, lastFolder)=> - lastFolder.should.deep.equal @rootFolder - assert.equal lastFolder.parentFolder_id, undefined - done() - - it 'should make just one folder', (done)-> - path = "/differentFolder/" - @subject.mkdirp project_id, path, {}, (err, folders, lastFolder)=> - folders.length.should.equal 1 - lastFolder.name.should.equal "differentFolder" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - it 'should make the final folder in path if it doesnt exist with one level', (done)-> - path = "level1/level2" - @subject.mkdirp project_id, path, {}, (err, folders, lastFolder)=> - folders.length.should.equal 1 - lastFolder.name.should.equal "level2" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - it 'should make the final folder in path if it doesnt exist with mutliple levels', (done)-> - path = "level1/level2/level3" - - @subject.mkdirp project_id, path, {}, (err, folders, lastFolder) => - folders.length.should.equal 2 - folders[0].name.should.equal "level2" - folders[0].parentFolder_id.should.equal @parentFolder_id - lastFolder.name.should.equal "level3" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - it 'should work with slashes either side', (done)-> - path = "/level1/level2/level3/" - - @subject.mkdirp project_id, path, {}, (err, folders, lastFolder)=> - folders.length.should.equal 2 - folders[0].name.should.equal "level2" - folders[0].parentFolder_id.should.equal @parentFolder_id - lastFolder.name.should.equal "level3" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - it 'should use a case-insensitive match by default', (done)-> - path = "/differentFolder/" - @subject.mkdirp project_id, path, {}, (err, folders, lastFolder)=> - @ProjectLocator.findElementByPath.calledWithMatch({exactCaseMatch:undefined}) - .should.equal true - done() - - it 'should use a case-sensitive match if exactCaseMatch option is set', (done)-> - path = "/differentFolder/" - @subject.mkdirp project_id, path, {exactCaseMatch:true}, (err, folders, lastFolder)=> - @ProjectLocator.findElementByPath.calledWithMatch({exactCaseMatch:true}) - .should.equal true - done() - - describe 'moveEntity', -> - beforeEach -> - @pathAfterMove = { - fileSystem: "/somewhere/else.txt" - } - - @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() - @ProjectEntityHandler.getAllEntitiesFromProject - .onFirstCall() - .yields(null, @oldDocs = ['old-doc'], @oldFiles = ['old-file']) - @ProjectEntityHandler.getAllEntitiesFromProject - .onSecondCall() - .yields(null, @newDocs = ['new-doc'], @newFiles = ['new-file']) - - @doc = {lines:["1234","312343d"], rev: "1234"} - @path = { mongo:"folders[0]", fileSystem:"/old_folder/somewhere.txt" } - @newProject = "new-project" - @ProjectLocator.findElement = sinon.stub() - .withArgs({@project, element_id: @docId, type: 'docs'}) - .yields(null, @doc, @path) - - @subject._checkValidMove = sinon.stub().yields() - - @subject._removeElementFromMongoArray = sinon.stub().yields(null, @newProject) - @subject._putElement = sinon.stub().yields(null, path: @pathAfterMove, @newProject) - - @subject.moveEntity project_id, doc_id, folder_id, "docs", @callback - - it 'should get the project', -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it 'should find the doc to move', -> - @ProjectLocator.findElement - .calledWith({element_id: doc_id, type: "docs", project: @project }) - .should.equal true - - it 'should check this is a valid move', -> - @subject._checkValidMove - .calledWith(@project, 'docs', @doc, @path, folder_id) - .should.equal true - - it "should put the element in the new folder", -> - @subject._putElement - .calledWith(@project, folder_id, @doc, "docs") - .should.equal true - - it 'should remove the element from its current position', -> - @subject._removeElementFromMongoArray - .calledWith(@ProjectModel, project_id, @path.mongo, doc_id) - .should.equal true - - it 'should remove the element from its current position after putting the element in the new folder', -> - @subject._removeElementFromMongoArray - .calledAfter(@subject._putElement) - .should.equal true - - it "calls the callback", -> - changes = { @oldDocs, @newDocs, @oldFiles, @newFiles, @newProject } - @callback.calledWith( - null, @project, @path.fileSystem, @pathAfterMove.fileSystem, @doc.rev, changes - ).should.equal true - - describe 'moveEntity must refuse to move the folder to a subfolder of itself', -> - beforeEach -> - @pathAfterMove = { - fileSystem: "/somewhere/else.txt" - } - - @doc = {lines:["1234","312343d"], rev: "1234"} - @path = { mongo:"folders[0]", fileSystem:"/old_folder/somewhere.txt" } - @newProject = "new-project" - @ProjectLocator.findElement = sinon.stub() - .withArgs({@project, element_id: @docId, type: 'docs'}) - .yields(null, @doc, @path) - - # return an error when moving a folder to a subfolder of itself - @subject._checkValidMove = sinon.stub().yields(new Error()) - - @subject._removeElementFromMongoArray = sinon.stub().yields(null, @project) - @subject._putElement = sinon.stub().yields(null, path: @pathAfterMove, @newProject) - - @subject.moveEntity project_id, doc_id, folder_id, "docs", @callback - - it 'should get the project', -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it 'should find the doc to move', -> - @ProjectLocator.findElement - .calledWith({element_id: doc_id, type: "docs", project: @project }) - .should.equal true - - it 'should check this is an invalid move', -> - @subject._checkValidMove - .calledWith(@project, 'docs', @doc, @path, folder_id) - .should.equal true - - it "should not put the element in the new folder", -> - @subject._putElement.called - .should.equal false - - it 'should not remove the element from its current position', -> - @subject._removeElementFromMongoArray.called - .should.equal false - - it "calls the callback with an error", -> - @callback.calledWith( - new Error() - ).should.equal true - - describe 'deleteEntity', -> - beforeEach -> - @path = mongo: "mongo.path", fileSystem: "/file/system/path" - @doc = _id: doc_id - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @doc, @path) - @subject._removeElementFromMongoArray = sinon.stub().yields() - @subject.deleteEntity project_id, doc_id, 'doc', @callback - - it "should get the project", -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it "should find the element", -> - @ProjectLocator.findElement - .calledWith({@project, element_id: @doc._id, type: 'doc'}) - .should.equal true - - it "should remove the element from the database", -> - @subject._removeElementFromMongoArray - .calledWith(@ProjectModel, project_id, @path.mongo, @doc._id) - .should.equal true - - it "calls the callbck", -> - @callback.calledWith(null, @doc, @path, @project).should.equal true - - describe "renameEntity", -> - beforeEach -> - @newName = "new.tex" - @path = mongo: "mongo.path", fileSystem: "/old.tex" - - @project = - _id: ObjectId(project_id) - rootFolder: [_id:ObjectId()] - @doc = _id: doc_id, name: "old.tex", rev: 1 - @folder = _id: folder_id - @newProject = "new-project" - - @ProjectGetter.getProjectWithoutLock = sinon.stub().yields(null, @project) - - @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() - @ProjectEntityHandler.getAllEntitiesFromProject - .onFirstCall() - .yields( null, @oldDocs = ['old-doc'], @oldFiles = ['old-file']) - @ProjectEntityHandler.getAllEntitiesFromProject - .onSecondCall() - .yields( null, @newDocs = ['new-doc'], @newFiles = ['new-file']) - - @ProjectLocator.findElement = sinon.stub().yields(null, @doc, @path, @folder) - @subject._checkValidElementName = sinon.stub().yields() - @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @newProject) - - @subject.renameEntity project_id, doc_id, 'doc', @newName, @callback - - it 'should get the project', -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it 'should find the doc', -> - @ProjectLocator.findElement - .calledWith({element_id: doc_id, type: 'doc', project: @project }) - .should.equal true - - it 'should check the new name is valid', -> - @subject._checkValidElementName - .calledWith(@folder, @newName) - .should.equal true - - it 'should update the doc name', -> - @ProjectModel.findOneAndUpdate - .calledWith( - { _id: project_id }, - { $set: { "mongo.path.name": @newName }, $inc: {"version": 1} }, - { new: true } - ).should.equal true - - it 'calls the callback', -> - changes = { @oldDocs, @newDocs, @oldFiles, @newFiles, @newProject } - @callback.calledWith( - null, @project, '/old.tex', '/new.tex', @doc.rev, changes - ).should.equal true - - describe 'addFolder', -> - beforeEach -> - @folderName = "folder1234" - @ProjectGetter.getProjectWithOnlyFolders = sinon.stub().callsArgWith(1, null, @project) - @subject._confirmFolder = sinon.stub().yields(folder_id) - @subject._putElement = sinon.stub().yields() - - @subject.addFolder project_id, folder_id, @folderName, @callback - - it 'gets the project', -> - @ProjectGetter.getProjectWithoutLock - .calledWith(project_id, {rootFolder:true, name:true, overleaf:true}) - .should.equal true - - it 'checks the parent folder exists', -> - @subject._confirmFolder - .calledWith(@project, folder_id) - .should.equal true - - it 'puts the element in mongo', -> - folderMatcher = sinon.match (folder) => - folder.name == @folderName - - @subject._putElement - .calledWithMatch(@project, folder_id, folderMatcher, 'folder') - .should.equal true - - it 'calls the callback', -> - folderMatcher = sinon.match (folder) => - folder.name == @folderName - - @callback.calledWithMatch(null, folderMatcher, folder_id).should.equal true - - describe '_removeElementFromMongoArray ', -> - beforeEach -> - @mongoPath = "folders[0].folders[5]" - @id = "12344" - @entityId = "5678" - @ProjectModel.update = sinon.stub().yields() - @ProjectModel.findOneAndUpdate = sinon.stub().yields(null, @project) - @subject._removeElementFromMongoArray @ProjectModel, @id, @mongoPath, @entityId, @callback - - it 'should pull', -> - @ProjectModel.findOneAndUpdate - .calledWith({ _id: @id }, { '$pull': { 'folders[0]': {_id: @entityId } }, '$inc': {'version': 1} }, {'new': true}) - .should.equal true - - it 'should call the callback', -> - @callback.calledWith(null, @project).should.equal true - - describe "_countElements", -> - beforeEach -> - @project = - _id: project_id, - rootFolder: [ - docs: [{_id:123}, {_id:345}] - fileRefs: [{_id:123}, {_id:345}, {_id:456}] - folders: [ - { - docs: [{_id:123}, {_id:345}, {_id:456}] - fileRefs:{} - folders: [ - { - docs:[_id:1234], - fileRefs:[{_id:23123}, {_id:123213}, {_id:2312}] - folders:[ - { - docs:[{_id:321321}, {_id:123213}] - fileRefs:[{_id:312321}] - folders:[] - } - ] - } - ] - },{ - docs:[{_id:123}, {_id:32131}] - fileRefs:[] - folders:[ - { - docs:[{_id:3123}] - fileRefs:[{_id:321321}, {_id:321321}, {_id:313122}] - folders:0 - } - ] - } - ] - ] - - it "should return the correct number", -> - expect(@subject._countElements @project).to.equal(26) - - it "should deal with null folders", -> - @project.rootFolder[0].folders[0].folders = undefined - expect(@subject._countElements @project).to.equal(17) - - it "should deal with null docs", -> - @project.rootFolder[0].folders[0].docs = undefined - expect(@subject._countElements @project).to.equal(23) - - it "should deal with null fileRefs", -> - @project.rootFolder[0].folders[0].folders[0].fileRefs = undefined - expect(@subject._countElements @project).to.equal(23) - - describe "_putElement", -> - beforeEach -> - @project = - _id: project_id - rootFolder: [_id:ObjectId()] - @folder = - _id: ObjectId() - name: "someFolder" - docs: [ {name: "another-doc.tex"} ] - fileRefs: [ {name: "another-file.tex"} ] - folders: [ {name: "another-folder"} ] - @doc = - _id: ObjectId() - name: "new.tex" - @path = mongo: "mongo.path", fileSystem: "/file/system/old.tex" - @ProjectLocator.findElement = sinon.stub().yields(null, @folder, @path) - @ProjectModel.findOneAndUpdate = sinon.stub().yields(null, @project) - - describe "updating the project", -> - it "should use the correct mongo path", (done)-> - @subject._putElement @project, @folder._id, @doc, "docs", (err)=> - @ProjectModel.findOneAndUpdate.args[0][0]._id.should.equal @project._id - assert.deepEqual @ProjectModel.findOneAndUpdate.args[0][1].$push[@path.mongo+".docs"], @doc - done() - - it "should return the project in the callback", (done)-> - @subject._putElement @project, @folder._id, @doc, "docs", (err, path, project)=> - assert.equal project, @project - done() - - it "should add an s onto the type if not included", (done)-> - @subject._putElement @project, @folder._id, @doc, "doc", (err)=> - assert.deepEqual @ProjectModel.findOneAndUpdate.args[0][1].$push[@path.mongo+".docs"], @doc - done() - - it "should not call update if element is null", (done)-> - @subject._putElement @project, @folder._id, null, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - done() - - it "should default to root folder insert", (done)-> - @subject._putElement @project, null, @doc, "doc", (err)=> - @ProjectLocator.findElement.args[0][0].element_id.should.equal @project.rootFolder[0]._id - done() - - it "should error if the element has no _id", (done)-> - doc = - name:"something" - @subject._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - done() - - it "should error if element name contains invalid characters", (done)-> - doc = - _id: ObjectId() - name: "something*bad" - @subject._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("invalid element name") - done() - - it "should error if element name is too long", (done)-> - doc = - _id: ObjectId() - name: new Array(200).join("long-") + "something" - @subject._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("invalid element name") - done() - - it "should error if the folder name is too long", (done)-> - @path = - mongo: "mongo.path", - fileSystem: new Array(200).join("subdir/") + "foo" - @ProjectLocator.findElement.callsArgWith(1, null, @folder, @path) - doc = - _id: ObjectId() - name: "something" - @subject._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("path too long") - done() - - it "should error if a document already exists with the same name", (done)-> - doc = - _id: ObjectId() - name: "another-doc.tex" - @subject._putElement @project, @folder, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("file already exists") - done() - - it "should error if a file already exists with the same name", (done)-> - doc = - _id: ObjectId() - name: "another-file.tex" - @subject._putElement @project, @folder, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("file already exists") - done() - - it "should error if a folder already exists with the same name", (done)-> - doc = - _id: ObjectId() - name: "another-folder" - @subject._putElement @project, @folder, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("file already exists") - done() - - describe '_checkValidElementName', -> - beforeEach -> - @folder = - docs: [ name: 'doc_name' ] - fileRefs: [ name: 'file_name' ] - folders: [ name: 'folder_name' ] - - it 'returns an error if name matches any doc name', -> - @subject._checkValidElementName @folder, 'doc_name', (err) -> - expect(err).to.deep.equal new Errors.InvalidNameError("file already exists") - - it 'returns an error if name matches any file name', -> - @subject._checkValidElementName @folder, 'file_name', (err) -> - expect(err).to.deep.equal new Errors.InvalidNameError("file already exists") - - it 'returns an error if name matches any folder name', -> - @subject._checkValidElementName @folder, 'folder_name', (err) -> - expect(err).to.deep.equal new Errors.InvalidNameError("file already exists") - - it 'returns nothing if name is valid', -> - @subject._checkValidElementName @folder, 'unique_name', (err) -> - expect(err).to.be.undefined - - describe '_checkValidMove', -> - beforeEach -> - @destFolder = _id: folder_id - @destFolderPath = fileSystem: '/foo/bar' - @ProjectLocator.findElement = sinon.stub().yields(null, @destFolder, @destFolderPath) - @subject._checkValidElementName = sinon.stub().yields() - - it 'checks the element name is valid', -> - @doc = _id: doc_id, name: 'doc_name' - @subject._checkValidMove @project, 'doc', @doc, fileSystem: '/main.tex', @destFolder._id, (err) => - expect(err).to.be.undefined - @subject._checkValidElementName - .calledWith(@destFolder, @doc.name) - .should.equal true - - it 'returns an error if trying to move a folder inside itself', -> - folder = name: 'folder_name' - @subject._checkValidMove @project, 'folder', folder, fileSystem: '/foo', @destFolder._id, (err) => - expect(err).to.deep.equal new Errors.InvalidNameError("destination folder is a child folder of me") - - describe "_insertDeletedDocReference", -> - beforeEach -> - @doc = - _id: ObjectId() - name: "test.tex" - @callback = sinon.stub() - @ProjectModel.update = sinon.stub().yields() - @subject._insertDeletedDocReference project_id, @doc, @callback - - it "should insert the doc into deletedDocs", -> - @ProjectModel.update - .calledWith({ - _id: project_id - }, { - $push: { - deletedDocs: { - _id: @doc._id - name: @doc.name - deletedAt: new Date() - } - } - }) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee deleted file mode 100644 index 65c0b74d50..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee +++ /dev/null @@ -1,1118 +0,0 @@ -chai = require('chai') -assert = require('chai').assert -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Project/ProjectEntityUpdateHandler" -sinon = require 'sinon' -Errors = require "../../../../app/js/Features/Errors/Errors" -SandboxedModule = require('sandboxed-module') -ObjectId = require("mongoose").Types.ObjectId - -describe 'ProjectEntityUpdateHandler', -> - project_id = '4eecb1c1bffa66588e0000a1' - projectHistoryId = '123456' - doc_id = '4eecb1c1bffa66588e0000a2' - file_id = "4eecaffcbffa66588e000009" - folder_id = "4eecaffcbffa66588e000008" - rootFolderId = "4eecaffcbffa66588e000007" - new_file_id = "4eecaffcbffa66588e000099" - userId = 1234 - - beforeEach -> - @project = - _id: project_id, - name: 'project name' - overleaf: - history: - id: projectHistoryId - @fileUrl = 'filestore.example.com/file' - @FileStoreHandler = {} - - @DocModel = class Doc - constructor:(options)-> - {@name, @lines} = options - @_id = doc_id - @rev = 0 - @FileModel = class File - constructor:(options)-> - {@name} = options - # use a new id for replacement files - if @name is 'dummy-upload-filename' - @._id = new_file_id - else - @._id = file_id - @rev = 0 - if options.linkedFileData? - @linkedFileData = options.linkedFileData - if options.hash? - @hash = options.hash - @docName = "doc-name" - @docLines = ['1234','abc'] - - @fileName = "something.jpg" - @fileSystemPath = "somehintg" - - @linkedFileData = {provider: 'url'} - - @source = 'editor' - @callback = sinon.stub() - @ProjectEntityUpdateHandler = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} - '../../models/Doc': Doc:@DocModel - '../Docstore/DocstoreManager': @DocstoreManager = {} - '../Errors/Errors': Errors - '../../Features/DocumentUpdater/DocumentUpdaterHandler':@DocumentUpdaterHandler = - updateProjectStructure: sinon.stub().yields() - '../../models/File': File:@FileModel - '../FileStore/FileStoreHandler':@FileStoreHandler - "../../infrastructure/LockManager":@LockManager = - runWithLock: - sinon.spy((namespace, id, runner, callback) -> runner(callback)) - '../../models/Project': Project:@ProjectModel = {} - "./ProjectGetter": @ProjectGetter = {} - './ProjectLocator': @ProjectLocator = {} - './ProjectUpdateHandler': @ProjectUpdater = {} - './ProjectEntityHandler': @ProjectEntityHandler = {} - './ProjectEntityMongoUpdateHandler': @ProjectEntityMongoUpdateHandler = {} - '../ThirdPartyDataStore/TpdsUpdateSender':@TpdsUpdateSender = - addFile: sinon.stub().yields() - - describe 'copyFileFromExistingProjectWithProject', -> - - beforeEach -> - @oldProject_id = "123kljadas" - @oldFileRef = {name:@fileName, _id:"oldFileRef"} - @ProjectEntityMongoUpdateHandler._confirmFolder = sinon.stub().yields(folder_id) - @ProjectEntityMongoUpdateHandler._putElement = sinon.stub().yields(null, {path:{fileSystem: @fileSystemPath}}) - @FileStoreHandler.copyFile = sinon.stub().yields(null, @fileUrl) - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject @project._id, @project, folder_id, @oldProject_id, @oldFileRef, userId, @callback - - it 'should copy the file in FileStoreHandler', -> - @FileStoreHandler.copyFile - .calledWith(@oldProject_id, @oldFileRef._id, project_id, file_id) - .should.equal true - - it 'should put file into folder by calling put element', -> - @ProjectEntityMongoUpdateHandler._putElement - .calledWithMatch(@project, folder_id, { _id: file_id, name: @fileName }, "file") - .should.equal true - - it 'should return doc and parent folder', -> - @callback.calledWithMatch(null,{ _id: file_id, name: @fileName }, folder_id).should.equal true - - it 'should call third party data store if versioning is enabled', -> - @TpdsUpdateSender.addFile.calledWith( - project_id: project_id - file_id: file_id - path: @fileSystemPath - rev: 0 - project_name: @project.name - ).should.equal true - - it "should should send the change in project structure to the doc updater", -> - changesMatcher = sinon.match (changes) => - { newFiles } = changes - return false if newFiles.length != 1 - newFile = newFiles[0] - newFile.file._id == file_id && - newFile.path == @fileSystemPath && - newFile.url == @fileUrl - - @DocumentUpdaterHandler.updateProjectStructure - .calledWithMatch(project_id, projectHistoryId, userId, changesMatcher) - .should.equal true - - describe 'copyFileFromExistingProjectWithProject, with linkedFileData and hash', -> - - beforeEach -> - @oldProject_id = "123kljadas" - @oldFileRef = { - _id:"oldFileRef", - name:@fileName, - linkedFileData: @linkedFileData - hash: "123456" - } - @ProjectEntityMongoUpdateHandler._confirmFolder = sinon.stub().yields(folder_id) - @ProjectEntityMongoUpdateHandler._putElement = sinon.stub().yields(null, {path:{fileSystem: @fileSystemPath}}) - @FileStoreHandler.copyFile = sinon.stub().yields(null, @fileUrl) - @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject @project._id, @project, folder_id, @oldProject_id, @oldFileRef, userId, @callback - - it 'should copy the file in FileStoreHandler', -> - @FileStoreHandler.copyFile - .calledWith(@oldProject_id, @oldFileRef._id, project_id, file_id) - .should.equal true - - it 'should put file into folder by calling put element, with the linkedFileData and hash', -> - @ProjectEntityMongoUpdateHandler._putElement - .calledWithMatch( - @project, - folder_id, - { _id: file_id, name: @fileName, linkedFileData: @linkedFileData, hash: "123456"}, - "file" - ) - .should.equal true - - describe 'updateDocLines', -> - beforeEach -> - @path = "/somewhere/something.tex" - @doc = { - _id: doc_id - } - @version = 42 - @ranges = {"mock":"ranges"} - @lastUpdatedAt = (new Date()).getTime() - @lastUpdatedBy = 'fake-last-updater-id' - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields(null, @project) - @ProjectLocator.findElement = sinon.stub().yields(null, @doc, {fileSystem: @path}) - @TpdsUpdateSender.addDoc = sinon.stub().yields() - @ProjectUpdater.markAsUpdated = sinon.stub() - @callback = sinon.stub() - - describe "when the doc has been modified", -> - beforeEach -> - @DocstoreManager.updateDoc = sinon.stub().yields(null, true, @rev = 5) - @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback - - it "should get the project without doc lines", -> - @ProjectGetter.getProjectWithoutDocLines - .calledWith(project_id) - .should.equal true - - it "should find the doc", -> - @ProjectLocator.findElement - .calledWith({ - project: @project - type: "docs" - element_id: doc_id - }) - .should.equal true - - it "should update the doc in the docstore", -> - @DocstoreManager.updateDoc - .calledWith(project_id, doc_id, @docLines, @version, @ranges) - .should.equal true - - it "should mark the project as updated", -> - sinon.assert.calledWith( - @ProjectUpdater.markAsUpdated, - project_id, - @lastUpdatedAt, - @lastUpdatedBy - ) - - it "should send the doc the to the TPDS", -> - @TpdsUpdateSender.addDoc - .calledWith({ - project_id: project_id - project_name: @project.name - doc_id: doc_id - rev: @rev - path: @path - }) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the doc has not been modified", -> - beforeEach -> - @DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5) - @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback - - it "should not mark the project as updated", -> - @ProjectUpdater.markAsUpdated.called.should.equal false - - it "should not send the doc the to the TPDS", -> - @TpdsUpdateSender.addDoc.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the doc has been deleted", -> - beforeEach -> - @project.deletedDocs = [ _id: doc_id ] - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields(null, @project) - @ProjectLocator.findElement = sinon.stub().yields(new Errors.NotFoundError) - @DocstoreManager.updateDoc = sinon.stub().yields() - @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback - - it "should update the doc in the docstore", -> - @DocstoreManager.updateDoc - .calledWith(project_id, doc_id, @docLines, @version, @ranges) - .should.equal true - - it "should not mark the project as updated", -> - @ProjectUpdater.markAsUpdated.called.should.equal false - - it "should not send the doc the to the TPDS", -> - @TpdsUpdateSender.addDoc.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the doc is not related to the project", -> - beforeEach -> - @ProjectLocator.findElement = sinon.stub().yields() - @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback - - it "should log out the error", -> - @logger.error - .calledWith( - project_id: project_id - doc_id: doc_id - lines: @docLines - "doc not found while updating doc lines" - ) - .should.equal true - - it "should return a not found error", -> - @callback.calledWith(new Errors.NotFoundError()).should.equal true - - describe "when the project is not found", -> - beforeEach -> - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields() - @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback - - it "should return a not found error", -> - @callback.calledWith(new Errors.NotFoundError()).should.equal true - - describe "setRootDoc", -> - it "should call Project.update", -> - rootDoc_id = "root-doc-id-123123" - @ProjectModel.update = sinon.stub() - @ProjectEntityUpdateHandler.setRootDoc project_id, rootDoc_id - @ProjectModel.update - .calledWith({_id : project_id}, {rootDoc_id}) - .should.equal true - - describe "unsetRootDoc", -> - it "should call Project.update", -> - @ProjectModel.update = sinon.stub() - @ProjectEntityUpdateHandler.unsetRootDoc project_id - @ProjectModel.update - .calledWith({_id : project_id}, {$unset : {rootDoc_id: true}}) - .should.equal true - - describe 'addDoc', -> - describe 'adding a doc', -> - beforeEach -> - @path = "/path/to/doc" - - @newDoc = name:@docName, lines:undefined, _id: doc_id, rev: 0 - @DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5) - @TpdsUpdateSender.addDoc = sinon.stub().yields() - @ProjectEntityMongoUpdateHandler.addDoc = sinon.stub().yields(null, {path: fileSystem: @path}, @project) - @ProjectEntityUpdateHandler.addDoc project_id, doc_id, @docName, @docLines, userId, @callback - - it "creates the doc without history", () -> - @DocstoreManager.updateDoc - .calledWith(project_id, doc_id, @docLines, 0, {}) - .should.equal true - - it "sends the change in project structure to the doc updater", () -> - newDocs = [ - doc: @newDoc - path: @path - docLines: @docLines.join('\n') - ] - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, {newDocs, newProject: @project}) - .should.equal true - - describe 'adding a doc with an invalid name', -> - beforeEach -> - @path = "/path/to/doc" - - @newDoc = _id: doc_id - @ProjectEntityUpdateHandler.addDoc project_id, folder_id, "*" + @docName, @docLines, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'addFile', -> - describe 'adding a file', -> - beforeEach -> - @path = "/path/to/file" - - @newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData} - @FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @fileUrl, @newFile) - @TpdsUpdateSender.addFile = sinon.stub().yields() - @ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project) - @ProjectEntityUpdateHandler.addFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback - - it "updates the file in the filestore", () -> - @FileStoreHandler.uploadFileFromDisk - .calledWith(project_id, {name:@fileName, linkedFileData:@linkedFileData}, @fileSystemPath) - .should.equal true - - it "updates the file in mongo", () -> - fileMatcher = sinon.match (file) => - file.name == @fileName - - @ProjectEntityMongoUpdateHandler.addFile - .calledWithMatch(project_id, folder_id, fileMatcher) - .should.equal true - - it "notifies the tpds", () -> - @TpdsUpdateSender.addFile - .calledWith({ - project_id: project_id - project_name: @project.name - file_id: file_id - rev: 0 - path: @path - }) - .should.equal true - - it "sends the change in project structure to the doc updater", () -> - newFiles = [ - file: @newFile - path: @path - url: @fileUrl - ] - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, {newFiles, newProject: @project}) - .should.equal true - - describe 'adding a file with an invalid name', -> - beforeEach -> - @path = "/path/to/file" - - @newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData} - @TpdsUpdateSender.addFile = sinon.stub().yields() - @ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project) - @ProjectEntityUpdateHandler.addFile project_id, folder_id, "*" + @fileName, @fileSystemPath, @linkedFileData, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'replaceFile', -> - beforeEach -> - # replacement file now creates a new file object - @newFileUrl = "new-file-url" - @FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @newFileUrl, @newFile) - - @newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0, linkedFileData: @linkedFileData - @oldFile = _id: file_id, rev: 3 - @path = "/path/to/file" - @newProject = "new project" - @FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @newFileUrl, @newFile) - @ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon.stub().yields() - @ProjectEntityMongoUpdateHandler.replaceFileWithNew = sinon.stub().yields(null, @oldFile, @project, fileSystem: @path, @newProject) - @ProjectEntityUpdateHandler.replaceFile project_id, file_id, @fileSystemPath, @linkedFileData, userId, @callback - - it 'uploads a new version of the file', -> - @FileStoreHandler.uploadFileFromDisk - .calledWith(project_id, {name:"dummy-upload-filename", linkedFileData:@linkedFileData}, @fileSystemPath) - .should.equal true - - it 'replaces the file in mongo', -> - @ProjectEntityMongoUpdateHandler.replaceFileWithNew - .calledWith(project_id, file_id, @newFile) - .should.equal true - - it 'notifies the tpds', -> - @TpdsUpdateSender.addFile - .calledWith({ - project_id: project_id - project_name: @project.name - file_id: new_file_id - rev: @oldFile.rev + 1 - path: @path - }) - .should.equal true - - it 'updates the project structure in the doc updater', -> - oldFiles = [ - file: @oldFile - path: @path - ] - newFiles = [ - file: @newFile - path: @path - url: @newFileUrl - ] - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, {oldFiles, newFiles, newProject: @newProject}) - .should.equal true - - describe 'upsertDoc', -> - describe 'upserting into an invalid folder', -> - beforeEach -> - @ProjectLocator.findElement = sinon.stub().yields() - @ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, @docName, @docLines, @source, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Error) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'updating an existing doc', -> - beforeEach -> - @existingDoc = _id: doc_id, name: @docName - @folder = _id: folder_id, docs: [@existingDoc] - @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @DocumentUpdaterHandler.setDocument = sinon.stub().yields() - @DocumentUpdaterHandler.flushDocToMongo = sinon.stub().yields() - - @ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, @docName, @docLines, @source, userId, @callback - - it 'tries to find the folder', -> - @ProjectLocator.findElement - .calledWith({project_id, element_id: folder_id, type: "folder"}) - .should.equal true - - it 'updates the doc contents', -> - @DocumentUpdaterHandler.setDocument - .calledWith(project_id, @existingDoc._id, userId, @docLines, @source) - .should.equal true - - it 'flushes the doc contents', -> - @DocumentUpdaterHandler.flushDocToMongo - .calledWith(project_id, @existingDoc._id ) - .should.equal true - - it 'returns the doc', -> - @callback.calledWith(null, @existingDoc, false) - - describe 'creating a new doc', -> - beforeEach -> - @folder = _id: folder_id, docs: [] - @newDoc = _id: doc_id - @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @ProjectEntityUpdateHandler.addDocWithRanges = withoutLock: sinon.stub().yields(null, @newDoc) - - @ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, @docName, @docLines, @source, userId, @callback - - it 'tries to find the folder', -> - @ProjectLocator.findElement - .calledWith({project_id, element_id: folder_id, type: "folder"}) - .should.equal true - - it 'adds the doc', -> - @ProjectEntityUpdateHandler.addDocWithRanges.withoutLock - .calledWith(project_id, folder_id, @docName, @docLines, {}, userId) - .should.equal true - - it 'returns the doc', -> - @callback.calledWith(null, @newDoc, true) - - describe 'upserting a new doc with an invalid name', -> - beforeEach -> - @folder = _id: folder_id, docs: [] - @newDoc = _id: doc_id - @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @ProjectEntityUpdateHandler.addDocWithRanges = withoutLock: sinon.stub().yields(null, @newDoc) - - @ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, "*" + @docName, @docLines, @source, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'upsertFile', -> - beforeEach -> - @FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @fileUrl, @newFile) - - describe 'upserting into an invalid folder', -> - beforeEach -> - @ProjectLocator.findElement = sinon.stub().yields() - @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Error) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'updating an existing file', -> - beforeEach -> - @existingFile = _id: file_id, name: @fileName - @folder = _id: folder_id, fileRefs: [@existingFile] - @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @ProjectEntityUpdateHandler.replaceFile = mainTask: sinon.stub().yields(null, @newFile) - - @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback - - it 'replaces the file', -> - @ProjectEntityUpdateHandler.replaceFile.mainTask - .calledWith(project_id, file_id, @fileSystemPath, @linkedFileData, userId) - .should.equal true - - it 'returns the file', -> - @callback.calledWith(null, @existingFile, false) - - describe 'creating a new file', -> - beforeEach -> - @folder = _id: folder_id, fileRefs: [] - @newFile = _id: file_id - @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @ProjectEntityUpdateHandler.addFile = mainTask: sinon.stub().yields(null, @newFile) - - @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback - - it 'tries to find the folder', -> - @ProjectLocator.findElement - .calledWith({project_id, element_id: folder_id, type: "folder"}) - .should.equal true - - it 'adds the file', -> - @ProjectEntityUpdateHandler.addFile.mainTask - .calledWith(project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId) - .should.equal true - - it 'returns the file', -> - @callback.calledWith(null, @newFile, true) - - describe 'upserting a new file with an invalid name', -> - beforeEach -> - @folder = _id: folder_id, fileRefs: [] - @newFile = _id: file_id - @ProjectLocator.findElement = sinon.stub().yields(null, @folder) - @ProjectEntityUpdateHandler.addFile = mainTask: sinon.stub().yields(null, @newFile) - - @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, '*' + @fileName, @fileSystemPath, @linkedFileData, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'upsertDocWithPath', -> - describe 'upserting a doc', -> - beforeEach -> - @path = "/folder/doc.tex" - @newFolders = [ 'mock-a', 'mock-b' ] - @folder = _id: folder_id - @doc = _id: doc_id - @isNewDoc = true - @ProjectEntityUpdateHandler.mkdirp = - withoutLock: sinon.stub().yields(null, @newFolders, @folder) - @ProjectEntityUpdateHandler.upsertDoc = - withoutLock: sinon.stub().yields(null, @doc, @isNewDoc) - - @ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback - - it 'creates any necessary folders', -> - @ProjectEntityUpdateHandler.mkdirp.withoutLock - .calledWith(project_id, '/folder') - .should.equal true - - it 'upserts the doc', -> - @ProjectEntityUpdateHandler.upsertDoc.withoutLock - .calledWith(project_id, @folder._id, 'doc.tex', @docLines, @source, userId) - .should.equal true - - it 'calls the callback', -> - @callback - .calledWith(null, @doc, @isNewDoc, @newFolders, @folder) - .should.equal true - - describe 'upserting a doc with an invalid path', -> - beforeEach -> - @path = "/*folder/doc.tex" - @newFolders = [ 'mock-a', 'mock-b' ] - @folder = _id: folder_id - @doc = _id: doc_id - @isNewDoc = true - @ProjectEntityUpdateHandler.mkdirp = - withoutLock: sinon.stub().yields(null, @newFolders, @folder) - @ProjectEntityUpdateHandler.upsertDoc = - withoutLock: sinon.stub().yields(null, @doc, @isNewDoc) - - @ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'upserting a doc with an invalid name', -> - beforeEach -> - @path = "/folder/*doc.tex" - @newFolders = [ 'mock-a', 'mock-b' ] - @folder = _id: folder_id - @doc = _id: doc_id - @isNewDoc = true - @ProjectEntityUpdateHandler.mkdirp = - withoutLock: sinon.stub().yields(null, @newFolders, @folder) - @ProjectEntityUpdateHandler.upsertDoc = - withoutLock: sinon.stub().yields(null, @doc, @isNewDoc) - - @ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'upsertFileWithPath', -> - describe 'upserting a file', -> - beforeEach -> - @path = "/folder/file.png" - @newFolders = [ 'mock-a', 'mock-b' ] - @folder = _id: folder_id - @file = _id: file_id - @isNewFile = true - @FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @fileUrl, @newFile) - @ProjectEntityUpdateHandler.mkdirp = - withoutLock: sinon.stub().yields(null, @newFolders, @folder) - @ProjectEntityUpdateHandler.upsertFile = - mainTask: sinon.stub().yields(null, @file, @isNewFile) - - @ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback - - it 'creates any necessary folders', -> - @ProjectEntityUpdateHandler.mkdirp.withoutLock - .calledWith(project_id, '/folder') - .should.equal true - - it 'upserts the file', -> - @ProjectEntityUpdateHandler.upsertFile.mainTask - .calledWith(project_id, @folder._id, 'file.png', @fileSystemPath, @linkedFileData, userId) - .should.equal true - - it 'calls the callback', -> - @callback - .calledWith(null, @file, @isNewFile, undefined, @newFolders, @folder) - .should.equal true - - describe 'upserting a file with an invalid path', -> - beforeEach -> - @path = "/*folder/file.png" - @newFolders = [ 'mock-a', 'mock-b' ] - @folder = _id: folder_id - @file = _id: file_id - @isNewFile = true - @ProjectEntityUpdateHandler.mkdirp = - withoutLock: sinon.stub().yields(null, @newFolders, @folder) - @ProjectEntityUpdateHandler.upsertFile = - mainTask: sinon.stub().yields(null, @file, @isNewFile) - - @ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'upserting a file with an invalid name', -> - beforeEach -> - @path = "/folder/*file.png" - @newFolders = [ 'mock-a', 'mock-b' ] - @folder = _id: folder_id - @file = _id: file_id - @isNewFile = true - @ProjectEntityUpdateHandler.mkdirp = - withoutLock: sinon.stub().yields(null, @newFolders, @folder) - @ProjectEntityUpdateHandler.upsertFile = - mainTask: sinon.stub().yields(null, @file, @isNewFile) - - @ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, @linkedFileData, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'deleteEntity', -> - beforeEach -> - @path = '/path/to/doc.tex' - @doc = _id: doc_id - @projectBeforeDeletion = _id: project_id, name: 'project' - @newProject = "new-project" - @ProjectEntityMongoUpdateHandler.deleteEntity = sinon.stub().yields(null, @doc, {fileSystem: @path}, @projectBeforeDeletion, @newProject) - @ProjectEntityUpdateHandler._cleanUpEntity = sinon.stub().yields() - @TpdsUpdateSender.deleteEntity = sinon.stub().yields() - - @ProjectEntityUpdateHandler.deleteEntity project_id, doc_id, 'doc', userId, @callback - - it 'deletes the entity in mongo', -> - @ProjectEntityMongoUpdateHandler.deleteEntity - .calledWith(project_id, doc_id, 'doc') - .should.equal true - - it 'cleans up the doc in the docstore', -> - @ProjectEntityUpdateHandler._cleanUpEntity - .calledWith(@projectBeforeDeletion, @newProject, @doc, 'doc', @path, userId) - .should.equal true - - it 'it notifies the tpds', -> - @TpdsUpdateSender.deleteEntity - .calledWith({ project_id, @path, project_name: @projectBeforeDeletion.name }) - .should.equal true - - it 'retuns the entity_id', -> - @callback.calledWith(null, doc_id).should.equal true - - describe 'deleteEntityWithPath', -> - describe 'when the entity exists', -> - beforeEach -> - @doc = _id: doc_id - @ProjectLocator.findElementByPath = sinon.stub().yields(null, @doc, 'doc') - @ProjectEntityUpdateHandler.deleteEntity = - withoutLock: sinon.stub().yields() - @path = '/path/to/doc.tex' - @ProjectEntityUpdateHandler.deleteEntityWithPath project_id, @path, userId, @callback - - it 'finds the entity', -> - @ProjectLocator.findElementByPath - .calledWith({project_id, @path}) - .should.equal true - - it 'deletes the entity', -> - @ProjectEntityUpdateHandler.deleteEntity.withoutLock - .calledWith(project_id, @doc._id, 'doc', userId, @callback) - .should.equal true - - describe 'when the entity does not exist', -> - beforeEach -> - @ProjectLocator.findElementByPath = sinon.stub().yields() - @path = '/doc.tex' - @ProjectEntityUpdateHandler.deleteEntityWithPath project_id, @path, userId, @callback - - it 'returns an error', -> - @callback.calledWith(new Errors.NotFoundError()).should.equal true - - describe 'mkdirp', -> - beforeEach -> - @docPath = '/folder/doc.tex' - @ProjectEntityMongoUpdateHandler.mkdirp = sinon.stub().yields() - @ProjectEntityUpdateHandler.mkdirp project_id, @docPath, @callback - - it 'calls ProjectEntityMongoUpdateHandler', -> - @ProjectEntityMongoUpdateHandler.mkdirp - .calledWith(project_id, @docPath) - .should.equal true - - describe 'mkdirpWithExactCase', -> - beforeEach -> - @docPath = '/folder/doc.tex' - @ProjectEntityMongoUpdateHandler.mkdirp = sinon.stub().yields() - @ProjectEntityUpdateHandler.mkdirpWithExactCase project_id, @docPath, @callback - - it 'calls ProjectEntityMongoUpdateHandler', -> - @ProjectEntityMongoUpdateHandler.mkdirp - .calledWith(project_id, @docPath, {exactCaseMatch: true}) - .should.equal true - - describe 'addFolder', -> - describe 'adding a folder', -> - beforeEach -> - @parentFolder_id = '123asdf' - @folderName = 'new-folder' - @ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields() - @ProjectEntityUpdateHandler.addFolder project_id, @parentFolder_id, @folderName, @callback - - it 'calls ProjectEntityMongoUpdateHandler', -> - @ProjectEntityMongoUpdateHandler.addFolder - .calledWith(project_id, @parentFolder_id, @folderName) - .should.equal true - - describe 'adding a folder with an invalid name', -> - beforeEach -> - @parentFolder_id = '123asdf' - @folderName = '*new-folder' - @ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields() - @ProjectEntityUpdateHandler.addFolder project_id, @parentFolder_id, @folderName, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe 'moveEntity', -> - beforeEach -> - @project_name = 'project name' - @startPath = '/a.tex' - @endPath = '/folder/b.tex' - @rev = 2 - @changes = newDocs: ['old-doc'], newFiles: ['old-file'] - @ProjectEntityMongoUpdateHandler.moveEntity = sinon.stub().yields( - null, @project, @startPath, @endPath, @rev, @changes - ) - @TpdsUpdateSender.moveEntity = sinon.stub() - @DocumentUpdaterHandler.updateProjectStructure = sinon.stub() - - @ProjectEntityUpdateHandler.moveEntity project_id, doc_id, folder_id, 'doc', userId, @callback - - it 'moves the entity in mongo', -> - @ProjectEntityMongoUpdateHandler.moveEntity - .calledWith(project_id, doc_id, folder_id, 'doc') - .should.equal true - - it 'notifies tpds', -> - @TpdsUpdateSender.moveEntity - .calledWith({project_id, @project_name, @startPath, @endPath, @rev}) - .should.equal true - - it 'sends the changes in project structure to the doc updater', -> - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, @changes, @callback) - .should.equal true - - describe "renameEntity", -> - describe 'renaming an entity', -> - beforeEach -> - @project_name = 'project name' - @startPath = '/folder/a.tex' - @endPath = '/folder/b.tex' - @rev = 2 - @changes = newDocs: ['old-doc'], newFiles: ['old-file'] - @newDocName = 'b.tex' - @ProjectEntityMongoUpdateHandler.renameEntity = sinon.stub().yields( - null, @project, @startPath, @endPath, @rev, @changes - ) - @TpdsUpdateSender.moveEntity = sinon.stub() - @DocumentUpdaterHandler.updateProjectStructure = sinon.stub() - - @ProjectEntityUpdateHandler.renameEntity project_id, doc_id, 'doc', @newDocName, userId, @callback - - it 'moves the entity in mongo', -> - @ProjectEntityMongoUpdateHandler.renameEntity - .calledWith(project_id, doc_id, 'doc', @newDocName) - .should.equal true - - it 'notifies tpds', -> - @TpdsUpdateSender.moveEntity - .calledWith({project_id, @project_name, @startPath, @endPath, @rev}) - .should.equal true - - it 'sends the changes in project structure to the doc updater', -> - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, @changes, @callback) - .should.equal true - - describe 'renaming an entity to an invalid name', -> - beforeEach -> - @project_name = 'project name' - @startPath = '/folder/a.tex' - @endPath = '/folder/b.tex' - @rev = 2 - @changes = newDocs: ['old-doc'], newFiles: ['old-file'] - @newDocName = '*b.tex' - @ProjectEntityMongoUpdateHandler.renameEntity = sinon.stub().yields( - null, @project, @startPath, @endPath, @rev, @changes - ) - @TpdsUpdateSender.moveEntity = sinon.stub() - @DocumentUpdaterHandler.updateProjectStructure = sinon.stub() - - @ProjectEntityUpdateHandler.renameEntity project_id, doc_id, 'doc', @newDocName, userId, @callback - - it 'returns an error', -> - errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) - @callback.calledWithMatch(errorMatcher) - .should.equal true - - describe "resyncProjectHistory", -> - describe "a deleted project", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub().yields() - - @ProjectEntityUpdateHandler.resyncProjectHistory project_id, @callback - - it "should return an error", -> - error = new Errors.ProjectHistoryDisabledError("project history not enabled for #{project_id}") - @callback.calledWith(error).should.equal true - - describe "a project without project-history enabled", -> - beforeEach -> - @project.overleaf = {} - @ProjectGetter.getProject = sinon.stub().yields(null, @project) - - @ProjectEntityUpdateHandler.resyncProjectHistory project_id, @callback - - it "should return an error", -> - error = new Errors.ProjectHistoryDisabledError("project history not enabled for #{project_id}") - @callback.calledWith(error).should.equal true - - describe "a project with project-history enabled", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub().yields(null, @project) - docs = [ - doc: _id: doc_id - path: 'main.tex' - ] - files = [ - file: _id: file_id - path: 'universe.png' - ] - @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().yields(null, docs, files) - @FileStoreHandler._buildUrl = (project_id, file_id) -> - "www.filestore.test/#{project_id}/#{file_id}" - @DocumentUpdaterHandler.resyncProjectHistory = sinon.stub().yields() - - @ProjectEntityUpdateHandler.resyncProjectHistory project_id, @callback - - it 'gets the project', -> - @ProjectGetter.getProject - .calledWith(project_id) - .should.equal true - - it 'gets the entities for the project', -> - @ProjectEntityHandler.getAllEntitiesFromProject - .calledWith(@project) - .should.equal true - - it 'tells the doc updater to sync the project', -> - docs = [ - doc: doc_id - path: 'main.tex' - ] - files = [ - file: file_id - path: 'universe.png' - url: "www.filestore.test/#{project_id}/#{file_id}" - ] - @DocumentUpdaterHandler.resyncProjectHistory - .calledWith(project_id, projectHistoryId, docs, files) - .should.equal true - - it 'calls the callback', -> - @callback.called.should.equal true - - describe "_cleanUpEntity", -> - beforeEach -> - @entity_id = "4eecaffcbffa66588e000009" - @FileStoreHandler.deleteFile = sinon.stub().yields() - @DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() - @ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() - @ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon.stub().yields() - - describe "a file", -> - beforeEach (done) -> - @path = "/file/system/path.png" - @entity = _id: @entity_id - @newProject = "new-project" - @ProjectEntityUpdateHandler._cleanUpEntity @project, @newProject, @entity, 'file', @path, userId, done - - it "should insert the file into the deletedFiles array", -> - @ProjectEntityMongoUpdateHandler._insertDeletedFileReference - .calledWith(@project._id, @entity) - .should.equal true - - it "should not delete the file from FileStoreHandler", -> - @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal false - - it "should not attempt to delete from the document updater", -> - @DocumentUpdaterHandler.deleteDoc.called.should.equal false - - it "should should send the update to the doc updater", -> - oldFiles = [ file: @entity, path: @path ] - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, {oldFiles, newProject: @newProject}) - .should.equal true - - describe "a doc", -> - beforeEach (done) -> - @path = "/file/system/path.tex" - @ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields() - @entity = {_id: @entity_id} - @newProject = "new-project" - @ProjectEntityUpdateHandler._cleanUpEntity @project, @newProject, @entity, 'doc', @path, userId, done - - it "should clean up the doc", -> - @ProjectEntityUpdateHandler._cleanUpDoc - .calledWith(@project, @entity, @path, userId) - .should.equal true - - it "should should send the update to the doc updater", -> - oldDocs = [ doc: @entity, path: @path ] - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, {oldDocs, newProject: @newProject}) - .should.equal true - - describe "a folder", -> - beforeEach (done) -> - @folder = - folders: [ - name: "subfolder" - fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ] - docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ] - folders: [] - ] - fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ] - docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ] - - @ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields() - @ProjectEntityUpdateHandler._cleanUpFile = sinon.stub().yields() - path = "/folder" - @newProject = "new-project" - @ProjectEntityUpdateHandler._cleanUpEntity @project, @newProject, @folder, "folder", path, userId, done - - it "should clean up all sub files", -> - @ProjectEntityUpdateHandler._cleanUpFile - .calledWith(@project, @file1, "/folder/subfolder/file-name-1", userId) - .should.equal true - @ProjectEntityUpdateHandler._cleanUpFile - .calledWith(@project, @file2, "/folder/file-name-2", userId) - .should.equal true - - it "should clean up all sub docs", -> - @ProjectEntityUpdateHandler._cleanUpDoc - .calledWith(@project, @doc1, "/folder/subfolder/doc-name-1", userId) - .should.equal true - @ProjectEntityUpdateHandler._cleanUpDoc - .calledWith(@project, @doc2, "/folder/doc-name-2", userId) - .should.equal true - - it "should should send one update to the doc updater for all docs and files", -> - oldFiles = [ {file: @file2, path: "/folder/file-name-2"}, {file: @file1, path: "/folder/subfolder/file-name-1"} ] - oldDocs = [ {doc: @doc2, path: "/folder/doc-name-2"}, { doc: @doc1, path: "/folder/subfolder/doc-name-1"} ] - @DocumentUpdaterHandler.updateProjectStructure - .calledWith(project_id, projectHistoryId, userId, {oldFiles, oldDocs, newProject: @newProject}) - .should.equal true - - describe "_cleanUpDoc", -> - beforeEach -> - @doc = - _id: ObjectId() - name: "test.tex" - @path = "/path/to/doc" - @ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() - @ProjectEntityMongoUpdateHandler._insertDeletedDocReference = sinon.stub().yields() - @DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() - @DocstoreManager.deleteDoc = sinon.stub().yields() - @callback = sinon.stub() - - describe "when the doc is the root doc", -> - beforeEach -> - @project.rootDoc_id = @doc._id - @ProjectEntityUpdateHandler._cleanUpDoc @project, @doc, @path, userId, @callback - - it "should unset the root doc", -> - @ProjectEntityUpdateHandler.unsetRootDoc - .calledWith(project_id) - .should.equal true - - it "should delete the doc in the doc updater", -> - @DocumentUpdaterHandler.deleteDoc - .calledWith(project_id, @doc._id.toString()) - - it "should insert the doc into the deletedDocs array", -> - @ProjectEntityMongoUpdateHandler._insertDeletedDocReference - .calledWith(@project._id, @doc) - .should.equal true - - it "should delete the doc in the doc store", -> - @DocstoreManager.deleteDoc - .calledWith(project_id, @doc._id.toString()) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the doc is not the root doc", -> - beforeEach -> - @project.rootDoc_id = ObjectId() - @ProjectEntityUpdateHandler._cleanUpDoc @project, @doc, @path, userId, @callback - - it "should not unset the root doc", -> - @ProjectEntityUpdateHandler.unsetRootDoc.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee b/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee deleted file mode 100644 index 18c6c14b31..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee +++ /dev/null @@ -1,258 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Project/ProjectGetter.js" -SandboxedModule = require('sandboxed-module') -ObjectId = require("mongojs").ObjectId -assert = require("chai").assert - -describe "ProjectGetter", -> - beforeEach -> - @callback = sinon.stub() - @ProjectGetter = SandboxedModule.require modulePath, requires: - "../../infrastructure/mongojs": - db: @db = - projects: {} - users: {} - ObjectId: ObjectId - "metrics-sharelatex": timeAsyncMethod: sinon.stub() - "../../models/Project": Project: @Project = {} - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} - "../../infrastructure/LockManager": @LockManager = - runWithLock : sinon.spy((namespace, id, runner, callback) -> runner(callback)) - './ProjectEntityMongoUpdateHandler': - lockKey: (project_id) -> project_id - "logger-sharelatex": - err:-> - log:-> - - describe "getProjectWithoutDocLines", -> - beforeEach -> - @project = - _id: @project_id = "56d46b0a1d3422b87c5ebcb1" - @ProjectGetter.getProject = sinon.stub().yields() - - describe "passing an id", -> - beforeEach -> - @ProjectGetter.getProjectWithoutDocLines @project_id, @callback - - it "should call find with the project id", -> - @ProjectGetter.getProject - .calledWith(@project_id) - .should.equal true - - it "should exclude the doc lines", -> - excludes = - "rootFolder.docs.lines": 0 - "rootFolder.folders.docs.lines": 0 - "rootFolder.folders.folders.docs.lines": 0 - "rootFolder.folders.folders.folders.docs.lines": 0 - "rootFolder.folders.folders.folders.folders.docs.lines": 0 - "rootFolder.folders.folders.folders.folders.folders.docs.lines": 0 - "rootFolder.folders.folders.folders.folders.folders.folders.docs.lines": 0 - "rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines": 0 - - @ProjectGetter.getProject - .calledWith(@project_id, excludes) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - - describe "getProjectWithOnlyFolders", -> - - beforeEach ()-> - @project = - _id: @project_id = "56d46b0a1d3422b87c5ebcb1" - @ProjectGetter.getProject = sinon.stub().yields() - - describe "passing an id", -> - beforeEach -> - @ProjectGetter.getProjectWithOnlyFolders @project_id, @callback - - it "should call find with the project id", -> - @ProjectGetter.getProject - .calledWith(@project_id) - .should.equal true - - it "should exclude the docs and files linesaaaa", -> - excludes = - "rootFolder.docs": 0 - "rootFolder.fileRefs": 0 - "rootFolder.folders.docs": 0 - "rootFolder.folders.fileRefs": 0 - "rootFolder.folders.folders.docs": 0 - "rootFolder.folders.folders.fileRefs": 0 - "rootFolder.folders.folders.folders.docs": 0 - "rootFolder.folders.folders.folders.fileRefs": 0 - "rootFolder.folders.folders.folders.folders.docs": 0 - "rootFolder.folders.folders.folders.folders.fileRefs": 0 - "rootFolder.folders.folders.folders.folders.folders.docs": 0 - "rootFolder.folders.folders.folders.folders.folders.fileRefs": 0 - "rootFolder.folders.folders.folders.folders.folders.folders.docs": 0 - "rootFolder.folders.folders.folders.folders.folders.folders.fileRefs": 0 - "rootFolder.folders.folders.folders.folders.folders.folders.folders.docs": 0 - "rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs": 0 - @ProjectGetter.getProject - .calledWith(@project_id, excludes) - .should.equal true - - it "should call the callback with the project", -> - @callback.called.should.equal true - - - describe "getProject", -> - beforeEach ()-> - @project = - _id: @project_id = "56d46b0a1d3422b87c5ebcb1" - @db.projects.find = sinon.stub().callsArgWith(2, null, [@project]) - - describe "without projection", -> - describe "with project id", -> - beforeEach -> - @ProjectGetter.getProject @project_id, @callback - - it "should call find with the project id", -> - expect(@db.projects.find.callCount).to.equal 1 - expect(@db.projects.find.lastCall.args[0]).to.deep.equal { - _id: ObjectId(@project_id) - } - - describe "without project id", -> - beforeEach -> - @ProjectGetter.getProject null, @callback - - it "should callback with error", -> - expect(@db.projects.find.callCount).to.equal 0 - expect(@callback.lastCall.args[0]).to.be.instanceOf Error - - describe "with projection", -> - beforeEach -> - @projection = {_id: 1} - - describe "with project id", -> - beforeEach -> - @ProjectGetter.getProject @project_id, @projection, @callback - - it "should call find with the project id", -> - expect(@db.projects.find.callCount).to.equal 1 - expect(@db.projects.find.lastCall.args[0]).to.deep.equal { - _id: ObjectId(@project_id) - } - expect(@db.projects.find.lastCall.args[1]).to.deep.equal @projection - - describe "without project id", -> - beforeEach -> - @ProjectGetter.getProject null, @callback - - it "should callback with error", -> - expect(@db.projects.find.callCount).to.equal 0 - expect(@callback.lastCall.args[0]).to.be.instanceOf Error - - describe "getProjectWithoutLock", -> - beforeEach ()-> - @project = - _id: @project_id = "56d46b0a1d3422b87c5ebcb1" - @db.projects.find = sinon.stub().callsArgWith(2, null, [@project]) - - describe "without projection", -> - describe "with project id", -> - beforeEach -> - @ProjectGetter.getProjectWithoutLock @project_id, @callback - - it "should call find with the project id", -> - expect(@db.projects.find.callCount).to.equal 1 - expect(@db.projects.find.lastCall.args[0]).to.deep.equal { - _id: ObjectId(@project_id) - } - - describe "without project id", -> - beforeEach -> - @ProjectGetter.getProjectWithoutLock null, @callback - - it "should callback with error", -> - expect(@db.projects.find.callCount).to.equal 0 - expect(@callback.lastCall.args[0]).to.be.instanceOf Error - - describe "with projection", -> - beforeEach -> - @projection = {_id: 1} - - describe "with project id", -> - beforeEach -> - @ProjectGetter.getProjectWithoutLock @project_id, @projection, @callback - - it "should call find with the project id", -> - expect(@db.projects.find.callCount).to.equal 1 - expect(@db.projects.find.lastCall.args[0]).to.deep.equal { - _id: ObjectId(@project_id) - } - expect(@db.projects.find.lastCall.args[1]).to.deep.equal @projection - - describe "without project id", -> - beforeEach -> - @ProjectGetter.getProjectWithoutLock null, @callback - - it "should callback with error", -> - expect(@db.projects.find.callCount).to.equal 0 - expect(@callback.lastCall.args[0]).to.be.instanceOf Error - - describe "findAllUsersProjects", -> - beforeEach -> - @fields = {"mock": "fields"} - @Project.find = sinon.stub() - @Project.find.withArgs({owner_ref: @user_id}, @fields).yields(null, ["mock-owned-projects"]) - @CollaboratorsHandler.getProjectsUserIsMemberOf = sinon.stub() - @CollaboratorsHandler.getProjectsUserIsMemberOf.withArgs(@user_id, @fields).yields( - null, - { - readAndWrite: ["mock-rw-projects"], - readOnly: ["mock-ro-projects"], - tokenReadAndWrite: ['mock-token-rw-projects'], - tokenReadOnly: ['mock-token-ro-projects'] - } - ) - @ProjectGetter.findAllUsersProjects @user_id, @fields, @callback - - it "should call the callback with all the projects", -> - @callback - .calledWith(null, { - owned: ["mock-owned-projects"], - readAndWrite: ["mock-rw-projects"], - readOnly: ["mock-ro-projects"] - tokenReadAndWrite: ['mock-token-rw-projects'], - tokenReadOnly: ['mock-token-ro-projects'] - }) - .should.equal true - - describe "getProjectIdByReadAndWriteToken", -> - describe "when project find returns project", -> - @beforeEach -> - @Project.findOne = sinon.stub().yields(null, {_id: "project-id"}) - @ProjectGetter.getProjectIdByReadAndWriteToken "token", @callback - - it "should find project with token", -> - @Project.findOne.calledWithMatch( - {'tokens.readAndWrite': "token"} - ).should.equal true - - it "should callback with project id", -> - @callback.calledWith(null, "project-id").should.equal true - - describe "when project not found", -> - @beforeEach -> - @Project.findOne = sinon.stub().yields() - @ProjectGetter.getProjectIdByReadAndWriteToken "token", @callback - - it "should callback empty", -> - expect(@callback.firstCall.args.length).to.equal 0 - - describe "when project find returns error", -> - @beforeEach -> - @Project.findOne = sinon.stub().yields("error") - @ProjectGetter.getProjectIdByReadAndWriteToken "token", @callback - - it "should callback with error", -> - @callback.calledWith("error").should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectHelperTests.coffee b/services/web/test/unit/coffee/Project/ProjectHelperTests.coffee deleted file mode 100644 index cab25e3a61..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectHelperTests.coffee +++ /dev/null @@ -1,26 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Project/ProjectHelper.js" -SandboxedModule = require('sandboxed-module') - -describe "ProjectHelper", -> - beforeEach -> - @ProjectHelper = SandboxedModule.require modulePath - - describe "compilerFromV1Engine", -> - it "returns the correct engine for latex_dvipdf", -> - expect(@ProjectHelper.compilerFromV1Engine('latex_dvipdf')).to.equal 'latex' - - it "returns the correct engine for pdflatex", -> - expect(@ProjectHelper.compilerFromV1Engine('pdflatex')).to.equal 'pdflatex' - - it "returns the correct engine for xelatex", -> - expect(@ProjectHelper.compilerFromV1Engine('xelatex')).to.equal 'xelatex' - - it "returns the correct engine for lualatex", -> - expect(@ProjectHelper.compilerFromV1Engine('lualatex')).to.equal 'lualatex' - - # describe "ensureNameIsUnique", -> - # see tests for: ProjectDetailsHandler.ensureProjectNameIsUnique, which calls here. \ No newline at end of file diff --git a/services/web/test/unit/coffee/Project/ProjectHistoryHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectHistoryHandlerTests.coffee deleted file mode 100644 index bb98b7bebc..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectHistoryHandlerTests.coffee +++ /dev/null @@ -1,105 +0,0 @@ -chai = require('chai') -assert = require('chai').assert -should = chai.should() -expect = chai.expect -sinon = require 'sinon' -modulePath = "../../../../app/js/Features/Project/ProjectHistoryHandler" -SandboxedModule = require('sandboxed-module') -ObjectId = require("mongoose").Types.ObjectId - -describe 'ProjectHistoryHandler', -> - project_id = '4eecb1c1bffa66588e0000a1' - userId = 1234 - - beforeEach -> - @ProjectModel = class Project - constructor:(options)-> - @._id = project_id - @name = "project_name_here" - @rev = 0 - rootFolder:[@rootFolder] - @project = new @ProjectModel() - - @callback = sinon.stub() - - @ProjectHistoryHandler = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} - 'settings-sharelatex': @Settings = {} - '../../models/Project': Project:@ProjectModel - './ProjectDetailsHandler': @ProjectDetailsHandler = {} - '../History/HistoryManager': @HistoryManager = {} - './ProjectEntityUpdateHandler': @ProjectEntityUpdateHandler = {} - - describe "starting history for an existing project", -> - beforeEach -> - @newHistoryId = 123456789 - @HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, {overleaf_id: @newHistoryId}) - @HistoryManager.flushProject = sinon.stub().callsArg(1) - @ProjectEntityUpdateHandler.resyncProjectHistory = sinon.stub().callsArg(1) - - describe "when the history does not already exist", -> - beforeEach -> - @ProjectDetailsHandler.getDetails = sinon.stub().withArgs(project_id).callsArgWith(1, null, @project) - @ProjectModel.update = sinon.stub().callsArgWith(2,null,{n:1}) - @ProjectHistoryHandler.ensureHistoryExistsForProject project_id, @callback - - it "should get any existing history id for the project", -> - @ProjectDetailsHandler.getDetails - .calledWith(project_id) - .should.equal true - - it "should initialize a new history in the v1 history service", -> - @HistoryManager.initializeProject - .called.should.equal.true - - it "should set the new history id on the project", -> - @ProjectModel.update - .calledWith({_id: project_id, "overleaf.history.id": {$exists:false}}, {"overleaf.history.id":@newHistoryId}) - .should.equal true - - it "should resync the project history", -> - @ProjectEntityUpdateHandler.resyncProjectHistory - .calledWith(project_id) - .should.equal true - - it "should flush the project history", -> - @HistoryManager.flushProject - .calledWith(project_id) - .should.equal true - - it "should call the callback without an error", -> - @callback.called.should.equal true - - describe "when the history already exists", -> - beforeEach -> - @project.overleaf = {history: {id: 1234}} - @ProjectDetailsHandler.getDetails = sinon.stub().withArgs(project_id).callsArgWith(1, null, @project) - @ProjectModel.update = sinon.stub() - @ProjectHistoryHandler.ensureHistoryExistsForProject project_id, @callback - - it "should get any existing history id for the project", -> - @ProjectDetailsHandler.getDetails - .calledWith(project_id) - .should.equal true - - it "should not initialize a new history in the v1 history service", -> - @HistoryManager.initializeProject - .called.should.equal false - - it "should not set the new history id on the project", -> - @ProjectModel.update - .called - .should.equal false - - it "should not resync the project history", -> - @ProjectEntityUpdateHandler.resyncProjectHistory - .called - .should.equal false - - it "should not flush the project history", -> - @HistoryManager.flushProject - .called - .should.equal false - - it "should call the callback", -> - @callback.calledWith().should.equal true \ No newline at end of file diff --git a/services/web/test/unit/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectLocatorTests.coffee deleted file mode 100644 index e19c481d8a..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectLocatorTests.coffee +++ /dev/null @@ -1,355 +0,0 @@ -spies = require('chai-spies') -chai = require('chai').use(spies) -assert = require('chai').assert -should = chai.should() -modulePath = "../../../../app/js/Features/Project/ProjectLocator" -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -Errors = require "../../../../app/js/Features/Errors/Errors" -expect = require("chai").expect -Project = class Project - -project = _id : "1234566", rootFolder:[] -rootDoc = name:"rootDoc", _id:"das239djd" -doc1 = name:"otherDoc.txt", _id:"dsad2ddd" -doc2 = name:"docname.txt", _id:"dsad2ddddd" -file1 = name:"file1", _id:"dsa9lkdsad" -subSubFile = name:"subSubFile", _id:"d1d2dk" -subSubDoc = name:"subdoc.txt", _id:"321dmdwi" -secondSubFolder = name:"secondSubFolder", _id:"dsa3e23", docs:[subSubDoc], fileRefs:[subSubFile], folders:[] -subFolder = name:"subFolder", _id:"dsadsa93", folders:[secondSubFolder, null], docs:[], fileRefs:[] -subFolder1 = name:"subFolder1", _id:"123asdjoij" - -rootFolder = - _id : "123sdskd" - docs:[doc1, doc2, null, rootDoc] - fileRefs:[file1] - folders:[subFolder1, subFolder] - -project.rootFolder[0] = rootFolder -project.rootDoc_id = rootDoc._id - - -describe 'ProjectLocator', -> - - beforeEach -> - Project.findById = (project_id, callback)=> - callback(null, project) - @ProjectGetter = - getProject: sinon.stub().callsArgWith(2, null, project) - @locator = SandboxedModule.require modulePath, requires: - '../../models/Project':{Project:Project} - '../../models/User':{User:@User} - "./ProjectGetter":@ProjectGetter - 'logger-sharelatex': - log:-> - err:-> - warn: -> - - describe 'finding a doc', -> - it 'finds one at the root level', (done)-> - @locator.findElement {project_id:project._id, element_id:doc2._id, type:"docs"}, (err, foundElement, path, parentFolder)-> - assert(!err?) - foundElement._id.should.equal doc2._id - path.fileSystem.should.equal "/#{doc2.name}" - parentFolder._id.should.equal project.rootFolder[0]._id - path.mongo.should.equal "rootFolder.0.docs.1" - done() - - it 'when it is nested', (done)-> - @locator.findElement {project_id:project._id, element_id:subSubDoc._id, type:"doc"}, (err, foundElement, path, parentFolder)-> - assert(!err?) - should.equal foundElement._id, subSubDoc._id - path.fileSystem.should.equal "/#{subFolder.name}/#{secondSubFolder.name}/#{subSubDoc.name}" - parentFolder._id.should.equal secondSubFolder._id - path.mongo.should.equal "rootFolder.0.folders.1.folders.0.docs.0" - done() - - it 'should give error if element could not be found', (done)-> - @locator.findElement {project_id:project._id, element_id:"ddsd432nj42", type:"docs"}, (err, foundElement, path, parentFolder)-> - err.should.deep.equal new Errors.NotFoundError("entity not found") - done() - - - describe 'finding a folder', -> - it 'should return root folder when looking for root folder', (done)-> - @locator.findElement {project_id:project._id, element_id:rootFolder._id, type:"folder"}, (err, foundElement, path, parentFolder)-> - assert(!err) - foundElement._id.should.equal rootFolder._id - done() - - it 'when at root', (done)-> - @locator.findElement {project_id:project._id, element_id:subFolder._id, type:"folder"}, (err, foundElement, path, parentFolder)-> - assert(!err) - foundElement._id.should.equal subFolder._id - path.fileSystem.should.equal "/#{subFolder.name}" - parentFolder._id.should.equal rootFolder._id - path.mongo.should.equal "rootFolder.0.folders.1" - done() - - it 'when deeply nested', (done)-> - @locator.findElement {project_id:project._id, element_id:secondSubFolder._id, type:"folder"}, (err, foundElement, path, parentFolder)-> - assert(!err) - foundElement._id.should.equal secondSubFolder._id - path.fileSystem.should.equal "/#{subFolder.name}/#{secondSubFolder.name}" - parentFolder._id.should.equal subFolder._id - path.mongo.should.equal "rootFolder.0.folders.1.folders.0" - done() - - describe 'finding a file', -> - it 'when at root', (done)-> - @locator.findElement {project_id:project._id, element_id:file1._id, type:"fileRefs"}, (err, foundElement, path, parentFolder)-> - assert(!err) - foundElement._id.should.equal file1._id - path.fileSystem.should.equal "/#{file1.name}" - parentFolder._id.should.equal rootFolder._id - path.mongo.should.equal "rootFolder.0.fileRefs.0" - done() - - it 'when deeply nested', (done)-> - @locator.findElement {project_id:project._id, element_id:subSubFile._id, type:"fileRefs"}, (err, foundElement, path, parentFolder)-> - assert(!err) - foundElement._id.should.equal subSubFile._id - path.fileSystem.should.equal "/#{subFolder.name}/#{secondSubFolder.name}/#{subSubFile.name}" - parentFolder._id.should.equal secondSubFolder._id - path.mongo.should.equal "rootFolder.0.folders.1.folders.0.fileRefs.0" - done() - - describe 'finding an element with wrong element type', -> - it 'should add an s onto the element type', (done)-> - @locator.findElement {project_id:project._id, element_id:subSubDoc._id, type:"doc"}, (err, foundElement, path, parentFolder)-> - assert(!err) - foundElement._id.should.equal subSubDoc._id - done() - - it 'should convert file to fileRefs', (done)-> - @locator.findElement {project_id:project._id, element_id:file1._id, type:"fileRefs"}, (err, foundElement, path, parentFolder)-> - assert(!err) - foundElement._id.should.equal file1._id - done() - - describe 'should be able to take actual project as well as id', -> - doc3 = - _id:"123dsdj3" - name:"doc3" - rootFolder2 = - _id : "123sddedskd" - docs:[doc3] - project2 = - _id : "1234566" - rootFolder:[rootFolder2] - it 'should find doc in project', (done)-> - @locator.findElement {project:project2, element_id:doc3._id, type:"docs"}, (err, foundElement, path, parentFolder)-> - assert(!err?) - foundElement._id.should.equal doc3._id - path.fileSystem.should.equal "/#{doc3.name}" - parentFolder._id.should.equal project2.rootFolder[0]._id - path.mongo.should.equal "rootFolder.0.docs.0" - done() - - describe 'finding root doc', -> - it 'should return root doc when passed project', (done)-> - @locator.findRootDoc project, (err, doc)-> - assert !err? - doc._id.should.equal rootDoc._id - done() - - it 'should return root doc when passed project_id', (done)-> - @locator.findRootDoc project._id, (err, doc)-> - assert !err? - doc._id.should.equal rootDoc._id - done() - - it 'should return null when the project has no rootDoc', (done) -> - project.rootDoc_id = null - @locator.findRootDoc project, (err, doc)-> - assert !err? - expect(doc).to.equal null - done() - - it 'should return null when the rootDoc_id no longer exists', (done) -> - project.rootDoc_id = "doesntexist" - @locator.findRootDoc project, (err, doc)-> - assert !err? - expect(doc).to.equal null - done() - - describe 'findElementByPath', -> - - it 'should take a doc path and return the element for a root level document', (done)-> - path = "#{doc1.name}" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal doc1 - expect(type).to.equal "doc" - done() - - it 'should take a doc path and return the element for a root level document with a starting slash', (done)-> - path = "/#{doc1.name}" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal doc1 - expect(type).to.equal "doc" - done() - - it 'should take a doc path and return the element for a nested document', (done)-> - path = "#{subFolder.name}/#{secondSubFolder.name}/#{subSubDoc.name}" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal subSubDoc - expect(type).to.equal "doc" - done() - - it 'should take a file path and return the element for a root level document', (done)-> - path = "#{file1.name}" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal file1 - expect(type).to.equal "file" - done() - - it 'should take a file path and return the element for a nested document', (done)-> - path = "#{subFolder.name}/#{secondSubFolder.name}/#{subSubFile.name}" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal subSubFile - expect(type).to.equal "file" - done() - - it 'should take a file path and return the element for a nested document case insenstive', (done)-> - path = "#{subFolder.name.toUpperCase()}/#{secondSubFolder.name.toUpperCase()}/#{subSubFile.name.toUpperCase()}" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal subSubFile - expect(type).to.equal "file" - done() - - it 'should take a file path and return the element for a nested folder', (done)-> - path = "#{subFolder.name}/#{secondSubFolder.name}" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal secondSubFolder - expect(type).to.equal "folder" - done() - - it 'should take a file path and return the root folder', (done)-> - path = "/" - @locator.findElementByPath {project, path}, (err, element, type)-> - element.should.deep.equal rootFolder - expect(type).to.equal "folder" - done() - - it 'should return an error if the file can not be found inside know folder', (done)-> - path = "#{subFolder.name}/#{secondSubFolder.name}/exist.txt" - @locator.findElementByPath {project, path}, (err, element, type)-> - err.should.not.equal undefined - assert.equal element, undefined - expect(type).to.be.undefined - done() - - it 'should return an error if the file can not be found inside unknown folder', (done)-> - path = "this/does/not/exist.txt" - @locator.findElementByPath {project, path}, (err, element, type)-> - err.should.not.equal undefined - assert.equal element, undefined - expect(type).to.be.undefined - done() - - - describe "where duplicate folder exists", -> - beforeEach -> - @duplicateFolder = {name:"duplicate1", _id:"1234", folders:[{ - name: "1" - docs:[{name:"main.tex", _id:"456"}] - folders: [] - fileRefs: [] - }], docs:[@doc = {name:"main.tex", _id:"456"}], fileRefs:[]} - @project = - rootFolder:[ - folders: [@duplicateFolder, @duplicateFolder] - fileRefs: [] - docs: [] - ] - - it "should not call the callback more than once", (done)-> - path = "#{@duplicateFolder.name}/#{@doc.name}" - @locator.findElementByPath {@project, path}, -> - done() #mocha will throw exception if done called multiple times - - it "should not call the callback more than once when the path is longer than 1 level below the duplicate level", (done)-> - path = "#{@duplicateFolder.name}/1/main.tex" - @locator.findElementByPath {@project, path}, -> - done() #mocha will throw exception if done called multiple times - - describe "with a null doc", -> - beforeEach -> - @project = - rootFolder:[ - folders: [] - fileRefs: [] - docs: [{name:"main.tex"}, null, {name:"other.tex"}] - ] - - it "should not crash with a null", (done)-> - path = "/other.tex" - @locator.findElementByPath {@project, path}, (err, element)-> - element.name.should.equal "other.tex" - done() - - describe "with a null project", -> - beforeEach -> - @ProjectGetter = - getProject: sinon.stub().callsArg(2) - - it "should not crash with a null", (done)-> - path = "/other.tex" - @locator.findElementByPath {project_id: project._id, path}, (err, element)-> - expect(err).to.exist - done() - - describe "with a project_id", -> - it 'should take a doc path and return the element for a root level document', (done)-> - path = "#{doc1.name}" - @locator.findElementByPath {project_id: project._id, path}, (err, element, type)=> - @ProjectGetter.getProject - .calledWith(project._id, {rootFolder:true, rootDoc_id: true}) - .should.equal true - element.should.deep.equal doc1 - expect(type).to.equal "doc" - done() - - describe 'findUsersProjectByName finding a project by user_id and project name', ()-> - it 'should return the project from an array case insenstive', (done)-> - user_id = "123jojoidns" - stubbedProject = {name:"findThis"} - projects = { - owned: [{name:"notThis"}, {name:"wellll"}, stubbedProject, {name:"Noooo"}] - } - @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) - @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> - project.should.equal stubbedProject - done() - - it 'should return the project which is not archived', (done)-> - user_id = "123jojoidns" - stubbedProject = {name:"findThis", _id:12331321} - projects = { - owned: [ - {name:"notThis"}, - {name:"wellll"}, - {name:"findThis",archived:true}, - stubbedProject, - {name:"findThis",archived:true}, - {name:"Noooo"} - ] - } - @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) - @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> - project._id.should.equal stubbedProject._id - done() - - it 'should search collab projects as well', (done)-> - user_id = "123jojoidns" - stubbedProject = {name:"findThis"} - projects = { - owned: [{name:"notThis"}, {name:"wellll"}, {name:"Noooo"}] - readAndWrite: [stubbedProject] - } - @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) - @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> - project.should.equal stubbedProject - done() - diff --git a/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee deleted file mode 100644 index 498fac355f..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectOptionsHandlerTests.coffee +++ /dev/null @@ -1,109 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -expect = chai.expect -should = chai.should() -modulePath = "../../../../app/js/Features/Project/ProjectOptionsHandler.js" -SandboxedModule = require('sandboxed-module') - -describe 'ProjectOptionsHandler', -> - project_id = "4eecaffcbffa66588e000008" - - beforeEach -> - @projectModel = class Project - constructor:(options)-> - @projectModel.update = sinon.spy() - - @handler = SandboxedModule.require modulePath, requires: - '../../models/Project':{Project:@projectModel} - 'settings-sharelatex': - languages:[ - {name: "English", code: "en"} - {name: "French", code: "fr"} - ] - imageRoot: "docker-repo/subdir" - allowedImageNames: [ - {imageName: "texlive-0000.0", imageDesc: "test image 0"} - {imageName: "texlive-1234.5", imageDesc: "test image 1"} - ] - 'logger-sharelatex': - log:-> - err:-> - - describe 'Setting the compiler', -> - it 'should perform and update on mongo', (done)-> - @handler.setCompiler project_id, "xeLaTeX", (err)=> - args = @projectModel.update.args[0] - args[0]._id.should.equal project_id - args[1].compiler.should.equal "xelatex" - done() - @projectModel.update.args[0][3]() - - it 'should not perform and update on mongo if it is not a reconised compiler', (done)-> - @handler.setCompiler project_id, "something", (err)=> - @projectModel.update.called.should.equal false - done() - - describe 'Setting the imageName', -> - it 'should perform and update on mongo', (done)-> - @handler.setImageName project_id, "texlive-1234.5", (err)=> - args = @projectModel.update.args[0] - args[0]._id.should.equal project_id - args[1].imageName.should.equal "docker-repo/subdir/texlive-1234.5" - done() - @projectModel.update.args[0][3]() - - it 'should not perform and update on mongo if it is not a reconised compiler', (done)-> - @handler.setImageName project_id, "something", (err)=> - @projectModel.update.called.should.equal false - done() - - describe "setting the spellCheckLanguage", -> - - it 'should perform and update on mongo', (done)-> - @handler.setSpellCheckLanguage project_id, "fr", (err)=> - args = @projectModel.update.args[0] - args[0]._id.should.equal project_id - args[1].spellCheckLanguage.should.equal "fr" - done() - @projectModel.update.args[0][3]() - - - it 'should not perform and update on mongo if it is not a reconised compiler', (done)-> - @handler.setSpellCheckLanguage project_id, "no a lang", (err)=> - @projectModel.update.called.should.equal false - done() - - it 'should perform and update on mongo if the language is blank (means turn it off)', (done)-> - @handler.setSpellCheckLanguage project_id, "", (err)=> - @projectModel.update.called.should.equal true - done() - @projectModel.update.args[0][3]() - - describe "setting the brandVariationId", -> - it 'should perform and update on mongo', (done)-> - @handler.setBrandVariationId project_id, "123", (err)=> - args = @projectModel.update.args[0] - args[0]._id.should.equal project_id - args[1].brandVariationId.should.equal "123" - done() - @projectModel.update.args[0][3]() - - - it 'should not perform and update on mongo if there is no brand variation', (done)-> - @handler.setBrandVariationId project_id, null, (err)=> - @projectModel.update.called.should.equal false - done() - - it 'should not perform and update on mongo if brand variation is an empty string', (done)-> - @handler.setBrandVariationId project_id, "", (err)=> - @projectModel.update.called.should.equal false - done() - - describe "unsetting the brandVariationId", -> - it 'should perform and update on mongo', (done)-> - @handler.unsetBrandVariationId project_id, (err)=> - args = @projectModel.update.args[0] - args[0]._id.should.equal project_id - expect(args[1]).to.deep.equal {$unset: {brandVariationId: 1}} - done() - @projectModel.update.args[0][3]() diff --git a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee b/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee deleted file mode 100644 index 0e42445a76..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee +++ /dev/null @@ -1,386 +0,0 @@ -chai = require('chai') -should = chai.should() -expect = chai.expect -sinon = require("sinon") -modulePath = "../../../../app/js/Features/Project/ProjectRootDocManager.js" -SandboxedModule = require('sandboxed-module') - -describe 'ProjectRootDocManager', -> - beforeEach -> - @project_id = "project-123" - @docPaths = - "doc-id-1": "/chapter1.tex" - "doc-id-2": "/main.tex" - "doc-id-3": "/nested/chapter1a.tex" - "doc-id-4": "/nested/chapter1b.tex" - @sl_req_id = "sl-req-id-123" - @callback = sinon.stub() - @globbyFiles = ['a.tex', 'b.tex', 'main.tex'] - @globby = sinon.stub().returns(new Promise (resolve) => - resolve(@globbyFiles) - ) - @fs = - readFile: sinon.stub().callsArgWith(2, new Error('file not found')) - stat: sinon.stub().callsArgWith(1, null, {size: 100}) - @ProjectRootDocManager = SandboxedModule.require modulePath, requires: - "./ProjectEntityHandler" : @ProjectEntityHandler = {} - "./ProjectEntityUpdateHandler" : @ProjectEntityUpdateHandler = {} - "./ProjectGetter" : @ProjectGetter = {} - "globby" : @globby - "fs" : @fs - - describe "setRootDocAutomatically", -> - describe "when there is a suitable root doc", -> - beforeEach (done)-> - @docs = - "/chapter1.tex": - _id: "doc-id-1" - lines: ["something else","\\begin{document}", "Hello world", "\\end{document}"] - "/main.tex": - _id: "doc-id-2" - lines: ["different line","\\documentclass{article}", "\\input{chapter1}"] - "/nested/chapter1a.tex": - _id: "doc-id-3" - lines: ["Hello world"] - "/nested/chapter1b.tex": - _id: "doc-id-4" - lines: ["Hello world"] - - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocAutomatically @project_id, done - - it "should check the docs of the project", -> - @ProjectEntityHandler.getAllDocs.calledWith(@project_id) - .should.equal true - - it "should set the root doc to the doc containing a documentclass", -> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") - .should.equal true - - describe "when the root doc is an Rtex file", -> - beforeEach -> - @docs = - "/chapter1.tex": - _id: "doc-id-1" - lines: ["\\begin{document}", "Hello world", "\\end{document}"] - "/main.Rtex": - _id: "doc-id-2" - lines: ["\\documentclass{article}", "\\input{chapter1}"] - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocAutomatically @project_id, @callback - - it "should set the root doc to the doc containing a documentclass", -> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") - .should.equal true - - describe "when there is no suitable root doc", -> - beforeEach (done)-> - @docs = - "/chapter1.tex": - _id: "doc-id-1" - lines: ["\\begin{document}", "Hello world", "\\end{document}"] - "/style.bst": - _id: "doc-id-2" - lines: ["%Example: \\documentclass{article}"] - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocAutomatically @project_id, done - - it "should not set the root doc to the doc containing a documentclass", -> - @ProjectEntityUpdateHandler.setRootDoc.called.should.equal false - - describe "findRootDocFileFromDirectory", -> - beforeEach -> - @fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'Hello World!') - @fs.readFile.withArgs('/foo/b.tex').callsArgWith(2, null, "I'm a little teapot, get me out of here.") - @fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, "Help, I'm trapped in a unit testing factory") - @fs.readFile.withArgs('/foo/c.tex').callsArgWith(2, null, 'Tomato, tomahto.') - @fs.readFile.withArgs('/foo/a/a.tex').callsArgWith(2, null, 'Potato? Potahto. Potootee!') - @documentclassContent = "% test\n\\documentclass\n\% test" - - describe "when there is a file in a subfolder", -> - beforeEach -> - # have to splice globbyFiles weirdly because of the way the stubbed globby method handles references - @globbyFiles.splice(0, @globbyFiles.length, 'c.tex', 'a.tex', 'a/a.tex', 'b.tex') - - it "processes the root folder files first, and then the subfolder, in alphabetical order", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', (error, path) => - expect(error).not.to.exist - expect(path).not.to.exist - sinon.assert.callOrder( - @fs.readFile.withArgs('/foo/a.tex') - @fs.readFile.withArgs('/foo/b.tex') - @fs.readFile.withArgs('/foo/c.tex') - @fs.readFile.withArgs('/foo/a/a.tex') - ) - done() - - it "processes smaller files first", (done) -> - @fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, {size: 1}) - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo',(error, path) => - expect(error).not.to.exist - expect(path).not.to.exist - sinon.assert.callOrder( - @fs.readFile.withArgs('/foo/c.tex') - @fs.readFile.withArgs('/foo/a.tex') - @fs.readFile.withArgs('/foo/b.tex') - @fs.readFile.withArgs('/foo/a/a.tex') - ) - done() - - describe "when main.tex contains a documentclass", -> - beforeEach -> - @fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, @documentclassContent) - - it "returns main.tex", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', (error, path, content) => - expect(error).not.to.exist - expect(path).to.equal 'main.tex' - expect(content).to.equal @documentclassContent - done() - - it "processes main.text first and stops processing when it finds the content", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', => - expect(@fs.readFile).to.be.calledWith('/foo/main.tex') - expect(@fs.readFile).not.to.be.calledWith('/foo/a.tex') - done() - - describe "when a.tex contains a documentclass", -> - beforeEach -> - @fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, @documentclassContent) - - it "returns a.tex", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', (error, path, content) => - expect(error).not.to.exist - expect(path).to.equal 'a.tex' - expect(content).to.equal @documentclassContent - done() - - it "processes main.text first and stops processing when it finds the content", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', => - expect(@fs.readFile).to.be.calledWith('/foo/main.tex') - expect(@fs.readFile).to.be.calledWith('/foo/a.tex') - expect(@fs.readFile).not.to.be.calledWith('/foo/b.tex') - done() - - describe "when there is no documentclass", -> - it "returns null with no error", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', (error, path, content) => - expect(error).not.to.exist - expect(path).not.to.exist - expect(content).not.to.exist - done() - - it "processes all the files", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', => - expect(@fs.readFile).to.be.calledWith('/foo/main.tex') - expect(@fs.readFile).to.be.calledWith('/foo/a.tex') - expect(@fs.readFile).to.be.calledWith('/foo/b.tex') - done() - - describe "when there is an error reading a file", -> - beforeEach -> - @fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, new Error('something went wrong')) - - it "returns an error", (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory '/foo', (error, path, content) => - expect(error).to.exist - expect(path).not.to.exist - expect(content).not.to.exist - done() - - describe "setRootDocFromName", -> - describe "when there is a suitable root doc", -> - beforeEach (done)-> - @docPaths = - "doc-id-1": "/chapter1.tex" - "doc-id-2": "/main.tex" - "doc-id-3": "/nested/chapter1a.tex" - "doc-id-4": "/nested/chapter1b.tex" - @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocFromName @project_id, '/main.tex', done - - it "should check the docs of the project", -> - @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) - .should.equal true - - it "should set the root doc to main.tex", -> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") - .should.equal true - - describe "when there is a suitable root doc but the leading slash is missing", -> - beforeEach (done)-> - @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocFromName @project_id, 'main.tex', done - - it "should check the docs of the project", -> - @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) - .should.equal true - - it "should set the root doc to main.tex", -> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") - .should.equal true - - describe "when there is a suitable root doc with a basename match", -> - beforeEach (done)-> - @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocFromName @project_id, 'chapter1a.tex', done - - it "should check the docs of the project", -> - @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) - .should.equal true - - it "should set the root doc using the basename", -> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-3") - .should.equal true - - describe "when there is a suitable root doc but the filename is in quotes", -> - beforeEach (done)-> - @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocFromName @project_id, "'main.tex'", done - - it "should check the docs of the project", -> - @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) - .should.equal true - - it "should set the root doc to main.tex", -> - @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") - .should.equal true - - describe "when there is no suitable root doc", -> - beforeEach (done)-> - @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) - @ProjectRootDocManager.setRootDocFromName @project_id, "other.tex", done - - it "should not set the root doc", -> - @ProjectEntityUpdateHandler.setRootDoc.called.should.equal false - - - describe "ensureRootDocumentIsSet", -> - beforeEach -> - @project = {} - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArgWith(1, null) - - describe "when the root doc is set", -> - beforeEach -> - @project.rootDoc_id = "root-doc-id" - @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) - - it "should find the project fetching only the rootDoc_id field", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootDoc_id: 1) - .should.equal true - - it "should not try to update the project rootDoc_id", -> - @ProjectRootDocManager.setRootDocAutomatically - .called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the root doc is not set", -> - beforeEach -> - @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) - - it "should find the project with only the rootDoc_id fiel", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootDoc_id: 1) - .should.equal true - - it "should update the project rootDoc_id", -> - @ProjectRootDocManager.setRootDocAutomatically - .calledWith(@project_id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the project does not exist", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) - @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) - - it "should call the callback with an error", -> - @callback.calledWith(new Error("project not found")).should.equal true - - describe "ensureRootDocumentIsValid", -> - beforeEach -> - @project = {} - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() - @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) - @ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArgWith(1, null) - - describe "when the root doc is set", -> - describe "when the root doc is valid", -> - beforeEach -> - @project.rootDoc_id = "doc-id-2" - @ProjectRootDocManager.ensureRootDocumentIsValid(@project_id, @callback) - - it "should find the project fetching only the rootDoc_id field", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootDoc_id: 1) - .should.equal true - - it "should not try to update the project rootDoc_id", -> - @ProjectRootDocManager.setRootDocAutomatically - .called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the root doc is not valid", -> - beforeEach -> - @project.rootDoc_id = "bogus-doc-id" - @ProjectRootDocManager.ensureRootDocumentIsValid(@project_id, @callback) - - it "should find the project fetching only the rootDoc_id field", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootDoc_id: 1) - .should.equal true - - it "should null the rootDoc_id field", -> - @ProjectEntityUpdateHandler.setRootDoc - .calledWith(@project_id, null) - .should.equal true - - it "should try to find a new rootDoc", -> - @ProjectRootDocManager.setRootDocAutomatically - .called.should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the root doc is not set", -> - beforeEach -> - @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) - - it "should find the project fetching only the rootDoc_id fiel", -> - @ProjectGetter.getProject - .calledWith(@project_id, rootDoc_id: 1) - .should.equal true - - it "should update the project rootDoc_id", -> - @ProjectRootDocManager.setRootDocAutomatically - .calledWith(@project_id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the project does not exist", -> - beforeEach -> - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) - @ProjectRootDocManager.ensureRootDocumentIsSet(@project_id, @callback) - - it "should call the callback with an error", -> - @callback.calledWith(new Error("project not found")).should.equal true - diff --git a/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee deleted file mode 100644 index 6f8408ce7f..0000000000 --- a/services/web/test/unit/coffee/Project/ProjectUpdateHandlerTests.coffee +++ /dev/null @@ -1,90 +0,0 @@ -sinon = require('sinon') -chai = require('chai').should() -modulePath = "../../../../app/js/Features/Project/ProjectUpdateHandler.js" -SandboxedModule = require('sandboxed-module') - -describe 'ProjectUpdateHandler', -> - - - before -> - @fakeTime = new Date() - @clock = sinon.useFakeTimers(@fakeTime.getTime()) - - beforeEach -> - @ProjectModel = class Project - @ProjectModel.update = sinon.stub().callsArg(3) - @handler = SandboxedModule.require modulePath, requires: - '../../models/Project':{Project:@ProjectModel} - 'logger-sharelatex' : { log: sinon.stub() } - - after -> - @clock.restore() - - describe 'marking a project as recently updated', -> - beforeEach -> - @project_id = "project_id" - @lastUpdatedAt = 987654321 - @lastUpdatedBy = 'fake-last-updater-id' - - it 'should send an update to mongo', (done)-> - @handler.markAsUpdated @project_id, @lastUpdatedAt, @lastUpdatedBy, (err) => - sinon.assert.calledWith( - @ProjectModel.update, - { - _id: @project_id, - lastUpdated: { $lt: @lastUpdatedAt } - }, - { - lastUpdated: @lastUpdatedAt, - lastUpdatedBy: @lastUpdatedBy - } - ) - done() - - it 'should set smart fallbacks', (done)-> - @handler.markAsUpdated @project_id, null, null, (err) => - sinon.assert.calledWithMatch( - @ProjectModel.update, - { - _id: @project_id, - lastUpdated: { $lt: @fakeTime } - }, - { - lastUpdated: @fakeTime - lastUpdatedBy: null - } - ) - done() - - describe "markAsOpened", -> - - it 'should send an update to mongo', (done)-> - project_id = "project_id" - @handler.markAsOpened project_id, (err)=> - args = @ProjectModel.update.args[0] - args[0]._id.should.equal project_id - date = args[1].lastOpened+"" - now = Date.now()+"" - date.substring(0,5).should.equal now.substring(0,5) - done() - - describe "markAsInactive", -> - - it 'should send an update to mongo', (done)-> - project_id = "project_id" - @handler.markAsInactive project_id, (err)=> - args = @ProjectModel.update.args[0] - args[0]._id.should.equal project_id - args[1].active.should.equal false - done() - - describe "markAsActive", -> - it 'should send an update to mongo', (done)-> - project_id = "project_id" - @handler.markAsActive project_id, (err)=> - args = @ProjectModel.update.args[0] - args[0]._id.should.equal project_id - args[1].active.should.equal true - done() - - diff --git a/services/web/test/unit/coffee/Project/SafePathTests.coffee b/services/web/test/unit/coffee/Project/SafePathTests.coffee deleted file mode 100644 index b726c7bf95..0000000000 --- a/services/web/test/unit/coffee/Project/SafePathTests.coffee +++ /dev/null @@ -1,205 +0,0 @@ -chai = require('chai') -assert = require('chai').assert -should = chai.should() -expect = chai.expect -sinon = require 'sinon' -modulePath = "../../../../app/js/Features/Project/SafePath" -SandboxedModule = require('sandboxed-module') - -describe 'SafePath', -> - beforeEach -> - @SafePath = SandboxedModule.require modulePath - - describe 'isCleanFilename', -> - it 'should accept a valid filename "main.tex"', -> - result = @SafePath.isCleanFilename 'main.tex' - result.should.equal true - - it 'should not accept an empty filename', -> - result = @SafePath.isCleanFilename '' - result.should.equal false - - it 'should not accept / anywhere', -> - result = @SafePath.isCleanFilename 'foo/bar' - result.should.equal false - - it 'should not accept .', -> - result = @SafePath.isCleanFilename '.' - result.should.equal false - - it 'should not accept ..', -> - result = @SafePath.isCleanFilename '..' - result.should.equal false - - it 'should not accept * anywhere', -> - result = @SafePath.isCleanFilename 'foo*bar' - result.should.equal false - - it 'should not accept leading whitespace', -> - result = @SafePath.isCleanFilename ' foobar.tex' - result.should.equal false - - it 'should not accept trailing whitespace', -> - result = @SafePath.isCleanFilename 'foobar.tex ' - result.should.equal false - - it 'should not accept leading and trailing whitespace', -> - result = @SafePath.isCleanFilename ' foobar.tex ' - result.should.equal false - - it 'should not accept control characters (0-31)', -> - result = @SafePath.isCleanFilename 'foo\u0010bar' - result.should.equal false - - it 'should not accept control characters (127, delete)', -> - result = @SafePath.isCleanFilename 'foo\u007fbar' - result.should.equal false - - it 'should not accept control characters (128-159)', -> - result = @SafePath.isCleanFilename 'foo\u0080\u0090bar' - result.should.equal false - - it 'should not accept surrogate characters (128-159)', -> - result = @SafePath.isCleanFilename 'foo\uD800\uDFFFbar' - result.should.equal false - - it 'should accept javascript property names', -> - result = @SafePath.isCleanFilename 'prototype' - result.should.equal true - - it 'should accept javascript property names in the prototype', -> - result = @SafePath.isCleanFilename 'hasOwnProperty' - result.should.equal true - - # this test never worked correctly because the spaces are not replaced by underscores in isCleanFilename - # it 'should not accept javascript property names resulting from substitutions', -> - # result = @SafePath.isCleanFilename ' proto ' - # result.should.equal false - - # it 'should not accept a trailing .', -> - # result = @SafePath.isCleanFilename 'hello.' - # result.should.equal false - - it 'should not accept \\', -> - result = @SafePath.isCleanFilename 'foo\\bar' - result.should.equal false - - describe 'isCleanPath', -> - it 'should accept a valid filename "main.tex"', -> - result = @SafePath.isCleanPath 'main.tex' - result.should.equal true - - it 'should accept a valid path "foo/main.tex"', -> - result = @SafePath.isCleanPath 'foo/main.tex' - result.should.equal true - - it 'should accept empty path elements', -> - result = @SafePath.isCleanPath 'foo//main.tex' - result.should.equal true - - it 'should not accept an empty filename', -> - result = @SafePath.isCleanPath 'foo/bar/' - result.should.equal false - - it 'should accept a path that starts with a slash', -> - result = @SafePath.isCleanPath '/etc/passwd' - result.should.equal true - - it 'should not accept a path that has an asterisk as the 0th element', -> - result = @SafePath.isCleanPath '*/foo/bar' - result.should.equal false - - it 'should not accept a path that has an asterisk as a middle element', -> - result = @SafePath.isCleanPath 'foo/*/bar' - result.should.equal false - - it 'should not accept a path that has an asterisk as the filename', -> - result = @SafePath.isCleanPath 'foo/bar/*' - result.should.equal false - - it 'should not accept a path that contains an asterisk in the 0th element', -> - result = @SafePath.isCleanPath 'f*o/bar/baz' - result.should.equal false - - it 'should not accept a path that contains an asterisk in a middle element', -> - result = @SafePath.isCleanPath 'foo/b*r/baz' - result.should.equal false - - it 'should not accept a path that contains an asterisk in the filename', -> - result = @SafePath.isCleanPath 'foo/bar/b*z' - result.should.equal false - - it 'should not accept multiple problematic elements', -> - result = @SafePath.isCleanPath 'f*o/b*r/b*z' - result.should.equal false - - it 'should not accept a problematic path with an empty element', -> - result = @SafePath.isCleanPath 'foo//*/bar' - result.should.equal false - - it 'should not accept javascript property names', -> - result = @SafePath.isCleanPath 'prototype' - result.should.equal false - - it 'should not accept javascript property names in the prototype', -> - result = @SafePath.isCleanPath 'hasOwnProperty' - result.should.equal false - - it 'should not accept javascript property names resulting from substitutions', -> - result = @SafePath.isCleanPath ' proto ' - result.should.equal false - - describe 'isAllowedLength', -> - it 'should accept a valid path "main.tex"', -> - result = @SafePath.isAllowedLength 'main.tex' - result.should.equal true - - it 'should not accept an extremely long path', -> - longPath = new Array(1000).join("/subdir") + '/main.tex' - result = @SafePath.isAllowedLength longPath - result.should.equal false - - it 'should not accept an empty path', -> - result = @SafePath.isAllowedLength '' - result.should.equal false - - describe 'clean', -> - it 'should not modify a valid filename', -> - result = @SafePath.clean 'main.tex' - result.should.equal 'main.tex' - - it 'should replace invalid characters with _', -> - result = @SafePath.clean 'foo/bar*/main.tex' - result.should.equal 'foo_bar__main.tex' - - it 'should replace "." with "_"', -> - result = @SafePath.clean '.' - result.should.equal '_' - - it 'should replace ".." with "__"', -> - result = @SafePath.clean '..' - result.should.equal '__' - - it 'should replace a single trailing space with _', -> - result = @SafePath.clean 'foo ' - result.should.equal 'foo_' - - it 'should replace a multiple trailing spaces with ___', -> - result = @SafePath.clean 'foo ' - result.should.equal 'foo__' - - it 'should replace a single leading space with _', -> - result = @SafePath.clean ' foo' - result.should.equal '_foo' - - it 'should replace a multiple leading spaces with ___', -> - result = @SafePath.clean ' foo' - result.should.equal '__foo' - - it 'should prefix javascript property names with @', -> - result = @SafePath.clean 'prototype' - result.should.equal '@prototype' - - it 'should prefix javascript property names in the prototype with @', -> - result = @SafePath.clean 'hasOwnProperty' - result.should.equal '@hasOwnProperty' diff --git a/services/web/test/unit/coffee/Publishers/PublishersGetterTests.coffee b/services/web/test/unit/coffee/Publishers/PublishersGetterTests.coffee deleted file mode 100644 index bda5cc935d..0000000000 --- a/services/web/test/unit/coffee/Publishers/PublishersGetterTests.coffee +++ /dev/null @@ -1,36 +0,0 @@ -SandboxedModule = require('sandboxed-module') -require('chai').should() -expect = require('chai').expect -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Publishers/PublishersGetter.js' - -describe 'PublishersGetter', -> - beforeEach -> - @publisher = - _id: 'mock-publsiher-id' - slug: 'ieee' - fetchV1Data: sinon.stub() - - @PublishersGetter = SandboxedModule.require modulePath, requires: - '../User/UserGetter': @UserGetter - "../UserMembership/UserMembershipsHandler": @UserMembershipsHandler = { - getEntitiesByUser: sinon.stub().callsArgWith(2, null, [@publisher]) - } - "../UserMembership/UserMembershipEntityConfigs": @UserMembershipEntityConfigs = { - publisher: - modelName: 'Publisher' - canCreate: true - fields: - primaryKey: 'slug' - } - 'logger-sharelatex': - log:-> console.log(arguments) - err:-> - - @userId = '12345abcde' - - describe "getManagedPublishers", -> - it 'fetches v1 data before returning publisher list', (done) -> - @PublishersGetter.getManagedPublishers @userId, (error, publishers) -> - publishers.length.should.equal 1 - done() diff --git a/services/web/test/unit/coffee/Referal/ReferalAllocatorTests.coffee b/services/web/test/unit/coffee/Referal/ReferalAllocatorTests.coffee deleted file mode 100644 index 498f0f0280..0000000000 --- a/services/web/test/unit/coffee/Referal/ReferalAllocatorTests.coffee +++ /dev/null @@ -1,94 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalAllocator.js' - -describe 'ReferalAllocator', -> - - beforeEach -> - @ReferalAllocator = SandboxedModule.require modulePath, requires: - '../../models/User': User: @User = {} - "../Subscription/FeaturesUpdater": @FeaturesUpdater = {} - "settings-sharelatex": @Settings = {} - 'logger-sharelatex': - log:-> - err:-> - @callback = sinon.stub() - @referal_id = "referal-id-123" - @referal_medium = "twitter" - @user_id = "user-id-123" - @new_user_id = "new-user-id-123" - @FeaturesUpdater.refreshFeatures = sinon.stub().yields() - @User.update = sinon.stub().callsArgWith 3, null - @User.findOne = sinon.stub().callsArgWith 1, null, { _id: @user_id } - - describe "allocate", -> - describe "when the referal was a bonus referal", -> - beforeEach -> - @referal_source = "bonus" - @ReferalAllocator.allocate @referal_id, @new_user_id, @referal_source, @referal_medium, @callback - - it 'should update the referring user with the refered users id', -> - @User.update.calledWith({ - "referal_id":@referal_id - }, { - $push: - refered_users: @new_user_id - $inc: - refered_user_count: 1 - }).should.equal true - - it "find the referring users id", -> - @User.findOne - .calledWith( referal_id: @referal_id ) - .should.equal true - - it "should refresh the user's subscription", -> - @FeaturesUpdater.refreshFeatures - .calledWith(@user_id) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when there is no user for the referal id", -> - beforeEach -> - @referal_source = "bonus" - @referal_id = "wombat" - @User.findOne = sinon.stub().callsArgWith 1, null, null - @ReferalAllocator.allocate @referal_id, @new_user_id, @referal_source, @referal_medium, @callback - - it "should find the referring users id", -> - @User.findOne - .calledWith( referal_id: @referal_id ) - .should.equal true - - it 'should not update the referring user with the refered users id', -> - @User.update.called.should.equal false - - it "should not assign the user a bonus", -> - @FeaturesUpdater.refreshFeatures.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - - describe "when the referal is not a bonus referal", -> - beforeEach -> - @referal_source = "public_share" - @ReferalAllocator.allocate @referal_id, @new_user_id, @referal_source, @referal_medium, @callback - - it 'should not update the referring user with the refered users id', -> - @User.update.called.should.equal false - - it "find the referring users id", -> - @User.findOne - .calledWith( referal_id: @referal_id ) - .should.equal true - - it "should not assign the user a bonus", -> - @FeaturesUpdater.refreshFeatures.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true diff --git a/services/web/test/unit/coffee/Referal/ReferalConnectTests.coffee b/services/web/test/unit/coffee/Referal/ReferalConnectTests.coffee deleted file mode 100644 index 7f168c2a93..0000000000 --- a/services/web/test/unit/coffee/Referal/ReferalConnectTests.coffee +++ /dev/null @@ -1,101 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalConnect.js' - -describe 'Referal connect middle wear', -> - - beforeEach -> - @connect = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': - log:-> - err:-> - - it 'should take a referal query string and put it on the session if it exists', (done)-> - req = - query: {referal : "12345"} - session : {} - @connect.use req, {}, -> - req.session.referal_id.should.equal(req.query.referal) - done() - - it 'should not change the referal_id on the session if not in query', (done)-> - req = - query: {} - session : {referal_id : "same"} - @connect.use req, {}, -> - req.session.referal_id.should.equal("same") - done() - - it 'should take a facebook referal query string and put it on the session if it exists', (done)-> - req = - query: {fb_ref : "12345"} - session : {} - @connect.use req, {}, -> - req.session.referal_id.should.equal(req.query.fb_ref) - done() - - it "should map the facebook medium into the session", (done) -> - req = - query: {rm : "fb"} - session : {} - @connect.use req, {}, -> - req.session.referal_medium.should.equal("facebook") - done() - - it "should map the twitter medium into the session", (done) -> - req = - query: {rm : "t"} - session : {} - @connect.use req, {}, -> - req.session.referal_medium.should.equal("twitter") - done() - - it "should map the google plus medium into the session", (done) -> - req = - query: {rm : "gp"} - session : {} - @connect.use req, {}, -> - req.session.referal_medium.should.equal("google_plus") - done() - - it "should map the email medium into the session", (done) -> - req = - query: {rm : "e"} - session : {} - @connect.use req, {}, -> - req.session.referal_medium.should.equal("email") - done() - - it "should map the direct medium into the session", (done) -> - req = - query: {rm : "d"} - session : {} - @connect.use req, {}, -> - req.session.referal_medium.should.equal("direct") - done() - - it "should map the bonus source into the session", (done) -> - req = - query: {rs : "b"} - session : {} - @connect.use req, {}, -> - req.session.referal_source.should.equal("bonus") - done() - - it "should map the public share source into the session", (done) -> - req = - query: {rs : "ps"} - session : {} - @connect.use req, {}, -> - req.session.referal_source.should.equal("public_share") - done() - - it "should map the collaborator invite into the session", (done) -> - req = - query: {rs : "ci"} - session : {} - @connect.use req, {}, -> - req.session.referal_source.should.equal("collaborator_invite") - done() diff --git a/services/web/test/unit/coffee/Referal/ReferalControllerTests.coffee b/services/web/test/unit/coffee/Referal/ReferalControllerTests.coffee deleted file mode 100644 index dbf7f94d60..0000000000 --- a/services/web/test/unit/coffee/Referal/ReferalControllerTests.coffee +++ /dev/null @@ -1,13 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalController.js' - -describe 'Referal controller', -> - - beforeEach -> - @controller = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': - log:-> - err:-> diff --git a/services/web/test/unit/coffee/Referal/ReferalFeaturesTests.coffee b/services/web/test/unit/coffee/Referal/ReferalFeaturesTests.coffee deleted file mode 100644 index dfa70f8ebe..0000000000 --- a/services/web/test/unit/coffee/Referal/ReferalFeaturesTests.coffee +++ /dev/null @@ -1,65 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalFeatures.js' - -describe 'ReferalFeatures', -> - - beforeEach -> - @ReferalFeatures = SandboxedModule.require modulePath, requires: - '../../models/User': User: @User = {} - "settings-sharelatex": @Settings = {} - 'logger-sharelatex': - log:-> - err:-> - @callback = sinon.stub() - @referal_id = "referal-id-123" - @referal_medium = "twitter" - @user_id = "user-id-123" - @new_user_id = "new-user-id-123" - - describe "getBonusFeatures", -> - beforeEach -> - @refered_user_count = 3 - @Settings.bonus_features = - "3": - collaborators: 3 - dropbox: false - versioning: false - stubbedUser = { - refered_user_count: @refered_user_count, - features:{collaborators:1, dropbox:false, versioning:false} - } - - @User.findOne = sinon.stub().callsArgWith 1, null, stubbedUser - @ReferalFeatures.getBonusFeatures @user_id, @callback - - it "should get the users number of refered user", -> - @User.findOne - .calledWith(_id: @user_id) - .should.equal true - - it "should call the callback with the features", -> - @callback.calledWith(null, @Settings.bonus_features[3]).should.equal true - - describe "when the user is not at a bonus level", -> - beforeEach -> - @refered_user_count = 0 - @Settings.bonus_features = - "1": - collaborators: 3 - dropbox: false - versioning: false - @User.findOne = sinon.stub().callsArgWith 1, null, { refered_user_count: @refered_user_count } - @ReferalFeatures.getBonusFeatures @user_id, @callback - - it "should get the users number of refered user", -> - @User.findOne - .calledWith(_id: @user_id) - .should.equal true - - it "should call the callback with no features", -> - @callback.calledWith(null, {}).should.equal true - - diff --git a/services/web/test/unit/coffee/Referal/ReferalHandlerTests.coffee b/services/web/test/unit/coffee/Referal/ReferalHandlerTests.coffee deleted file mode 100644 index f9021ecfff..0000000000 --- a/services/web/test/unit/coffee/Referal/ReferalHandlerTests.coffee +++ /dev/null @@ -1,64 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Referal/ReferalHandler.js' - -describe 'Referal handler', -> - - beforeEach -> - @User = findById:sinon.stub() - @handler = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': - log:-> - err:-> - '../../models/User': User:@User - @user_id = "12313" - - - describe 'getting refered user_ids', -> - it 'should get the user from mongo and return the refered users array', (done)-> - user = - refered_users : ["1234", "312312", "3213129"] - refered_user_count : 3 - @User.findById.callsArgWith(1, null, user) - - @handler.getReferedUsers @user_id, (err, passedReferedUserIds, passedReferedUserCount)-> - passedReferedUserIds.should.deep.equal user.refered_users - passedReferedUserCount.should.equal 3 - done() - - it 'should return an empty array if it is not set', (done)-> - user = {} - @User.findById.callsArgWith(1, null, user) - - @handler.getReferedUsers @user_id, (err, passedReferedUserIds, passedReferedUserCount)-> - passedReferedUserIds.length.should.equal 0 - done() - - it 'should return a zero count if netither it or the array are set', (done)-> - user = {} - @User.findById.callsArgWith(1, null, user) - - @handler.getReferedUsers @user_id, (err, passedReferedUserIds, passedReferedUserCount)-> - passedReferedUserCount.should.equal 0 - done() - - it 'should return the array length if count is not set', (done)-> - user = - refered_users : ["1234", "312312", "3213129"] - @User.findById.callsArgWith(1, null, user) - - @handler.getReferedUsers @user_id, (err, passedReferedUserIds, passedReferedUserCount)-> - passedReferedUserCount.should.equal 3 - done() - - it 'should return the count if it differs from the array length', (done)-> - user = - refered_users : ["1234", "312312", "3213129"] - refered_user_count : 5 - @User.findById.callsArgWith(1, null, user) - - @handler.getReferedUsers @user_id, (err, passedReferedUserIds, passedReferedUserCount)-> - passedReferedUserCount.should.equal 5 - done() diff --git a/services/web/test/unit/coffee/References/ReferencesControllerTests.coffee b/services/web/test/unit/coffee/References/ReferencesControllerTests.coffee deleted file mode 100644 index c612967c8d..0000000000 --- a/services/web/test/unit/coffee/References/ReferencesControllerTests.coffee +++ /dev/null @@ -1,244 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -sinon = require 'sinon' -assert = require("chai").assert -modulePath = "../../../../app/js/Features/References/ReferencesController" -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" - -describe "ReferencesController", -> - - beforeEach -> - @projectId = '2222' - @controller = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': { - log: -> - err: -> - }, - 'settings-sharelatex': @settings = { - apis: {web: {url: 'http://some.url'}} - }, - './ReferencesHandler': @ReferencesHandler = { - index: sinon.stub() - indexAll: sinon.stub() - }, - '../Editor/EditorRealTimeController': @EditorRealTimeController = { - emitToRoom: sinon.stub() - } - @req = new MockRequest() - @req.params.Project_id = @projectId - @req.body = - docIds: @docIds = ['aaa', 'bbb'] - shouldBroadcast: false - @res = new MockResponse() - @res.json = sinon.stub() - @res.send = sinon.stub() - @res.sendStatus = sinon.stub() - @fakeResponseData = - projectId: @projectId, - keys: ['one', 'two', 'three'] - - describe 'indexAll', -> - - beforeEach -> - @req.body = {shouldBroadcast: false} - @ReferencesHandler.indexAll.callsArgWith(1, null, @fakeResponseData) - @call = (callback) => - @controller.indexAll @req, @res - callback() - - it 'should not produce an error', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 0 - @res.sendStatus.calledWith(500).should.equal false - @res.sendStatus.calledWith(400).should.equal false - done() - - it 'should return data', (done) -> - @call () => - @res.json.callCount.should.equal 1 - @res.json.calledWith(@fakeResponseData).should.equal true - done() - - it 'should call ReferencesHandler.indexAll', (done) -> - @call () => - @ReferencesHandler.indexAll.callCount.should.equal 1 - @ReferencesHandler.indexAll.calledWith(@projectId).should.equal true - done() - - describe 'when shouldBroadcast is true', -> - - beforeEach -> - @ReferencesHandler.index.callsArgWith(2, null, @fakeResponseData) - @req.body.shouldBroadcast = true - - it 'should call EditorRealTimeController.emitToRoom', (done) -> - @call () => - @EditorRealTimeController.emitToRoom.callCount.should.equal 1 - done() - - it 'should not produce an error', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 0 - @res.sendStatus.calledWith(500).should.equal false - @res.sendStatus.calledWith(400).should.equal false - done() - - it 'should still return data', (done) -> - @call () => - @res.json.callCount.should.equal 1 - @res.json.calledWith(@fakeResponseData).should.equal true - done() - - describe 'when shouldBroadcast is false', -> - - beforeEach -> - @ReferencesHandler.index.callsArgWith(2, null, @fakeResponseData) - @req.body.shouldBroadcast = false - - it 'should not call EditorRealTimeController.emitToRoom', (done) -> - @call () => - @EditorRealTimeController.emitToRoom.callCount.should.equal 0 - done() - - it 'should not produce an error', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 0 - @res.sendStatus.calledWith(500).should.equal false - @res.sendStatus.calledWith(400).should.equal false - done() - - it 'should still return data', (done) -> - @call () => - @res.json.callCount.should.equal 1 - @res.json.calledWith(@fakeResponseData).should.equal true - done() - - describe 'there is no data', -> - - beforeEach -> - @ReferencesHandler.indexAll.callsArgWith(1) - @call = (callback) => - @controller.indexAll @req, @res - callback() - - it 'should not call EditorRealTimeController.emitToRoom', (done) -> - @call () => - @EditorRealTimeController.emitToRoom.callCount.should.equal 0 - done() - - it 'should not produce an error', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 0 - @res.sendStatus.calledWith(500).should.equal false - @res.sendStatus.calledWith(400).should.equal false - done() - - it 'should send a response with an empty keys list', (done) -> - @call () => - @res.json.called.should.equal true - @res.json.calledWith({projectId: @projectId, keys: []}).should.equal true - done() - - describe 'index', -> - - beforeEach -> - @call = (callback) => - @controller.index @req, @res - callback() - - describe 'with docIds as an array and shouldBroadcast as false', -> - - beforeEach -> - @ReferencesHandler.index.callsArgWith(2, null, @fakeResponseData) - - it 'should call ReferencesHandler.index', (done) -> - @call () => - @ReferencesHandler.index.callCount.should.equal 1 - @ReferencesHandler.index.calledWith(@projectId, @docIds).should.equal true - done() - - it 'should return data', (done) -> - @call () => - @res.json.callCount.should.equal 1 - @res.json.calledWith(@fakeResponseData).should.equal true - done() - - it 'should not produce an error', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 0 - @res.sendStatus.calledWith(500).should.equal false - @res.sendStatus.calledWith(400).should.equal false - done() - - it 'should not call EditorRealTimController.emitToRoom', (done) -> - @call () => - @EditorRealTimeController.emitToRoom.callCount.should.equal 0 - done() - - describe 'when ReferencesHandler.index produces an error', -> - - beforeEach -> - @ReferencesHandler.index.callsArgWith(2, new Error('woops'), null) - - it 'should produce an error response', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(500).should.equal true - done() - - describe 'when shouldBroadcast is true', -> - - beforeEach -> - @ReferencesHandler.index.callsArgWith(2, null, @fakeResponseData) - @req.body.shouldBroadcast = true - - it 'should call EditorRealTimeController.emitToRoom', (done) -> - @call () => - @EditorRealTimeController.emitToRoom.callCount.should.equal 1 - done() - - it 'should not produce an error', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 0 - @res.sendStatus.calledWith(500).should.equal false - @res.sendStatus.calledWith(400).should.equal false - done() - - it 'should still return data', (done) -> - @call () => - @res.json.callCount.should.equal 1 - @res.json.calledWith(@fakeResponseData).should.equal true - done() - - describe 'with missing docIds', -> - - beforeEach -> - delete @req.body.docIds - - it 'should produce an error response', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(400).should.equal true - done() - - it 'should not call ReferencesHandler.index', (done) -> - @call () => - @ReferencesHandler.index.callCount.should.equal 0 - done() - - describe 'with invalid docIds', -> - - beforeEach -> - @req.body.docIds = 42 - - it 'should produce an error response', (done) -> - @call () => - @res.sendStatus.callCount.should.equal 1 - @res.sendStatus.calledWith(400).should.equal true - done() - - it 'should not call ReferencesHandler.index', (done) -> - @call () => - @ReferencesHandler.index.callCount.should.equal 0 - done() diff --git a/services/web/test/unit/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/unit/coffee/References/ReferencesHandlerTests.coffee deleted file mode 100644 index 9ca565b24d..0000000000 --- a/services/web/test/unit/coffee/References/ReferencesHandlerTests.coffee +++ /dev/null @@ -1,397 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -expect = require('chai').expect -sinon = require 'sinon' -assert = require("chai").assert -modulePath = "../../../../app/js/Features/References/ReferencesHandler" - -describe 'ReferencesHandler', -> - - beforeEach -> - @projectId = '222' - @fakeProject = - _id: @projectId - owner_ref: @fakeOwner = - _id: 'some_owner' - features: - references: false - rootFolder: [ - docs: [ - {name: 'one.bib', _id: 'aaa'}, - {name: 'two.txt', _id: 'bbb'}, - ] - folders: [ - { - docs: [{name: 'three.bib', _id: 'ccc'}], - fileRefs: [{name: 'four.bib', _id: 'ghg'}], - folders: [] - } - ] - ] - @docIds = ['aaa', 'ccc'] - @handler = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': { - log: -> - err: -> - } - 'settings-sharelatex': @settings = { - apis: - references: {url: 'http://some.url/references'} - docstore: {url: 'http://some.url/docstore'} - filestore: {url: 'http://some.url/filestore'} - } - 'request': @request = { - get: sinon.stub() - post: sinon.stub() - } - '../Project/ProjectGetter': @ProjectGetter = { - getProject: sinon.stub().callsArgWith(2, null, @fakeProject) - } - '../User/UserGetter': @UserGetter = { - getUser: sinon.stub() - } - '../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = { - flushDocToMongo: sinon.stub().callsArgWith(2, null) - } - @fakeResponseData = - projectId: @projectId - keys: ['k1', 'k2'] - - describe 'index', -> - - beforeEach -> - sinon.stub(@handler, '_findBibDocIds') - sinon.stub(@handler, '_findBibFileIds') - sinon.stub(@handler, '_isFullIndex').callsArgWith(1, null, true) - @request.post.callsArgWith(1, null, {statusCode: 200}, @fakeResponseData) - @call = (callback) => - @handler.index @projectId, @docIds, callback - - describe 'with docIds as an array', -> - - beforeEach -> - @docIds = ['aaa', 'ccc'] - - it 'should not call _findBibDocIds', (done) -> - @call (err, data) => - @handler._findBibDocIds.callCount.should.equal 0 - done() - - it 'should call ProjectGetter.getProject', (done) -> - @call (err, data) => - @ProjectGetter.getProject.callCount.should.equal 1 - @ProjectGetter.getProject.calledWith(@projectId).should.equal true - done() - - it 'should not call _findBibDocIds', (done) -> - @call (err, data) => - @handler._findBibDocIds.callCount.should.equal 0 - done() - - it 'should call DocumentUpdaterHandler.flushDocToMongo', (done) -> - @call (err, data) => - @DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal 2 - @docIds.forEach (docId) => - @DocumentUpdaterHandler.flushDocToMongo.calledWith(@projectId, docId).should.equal true - done() - - it 'should make a request to references service', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 1 - arg = @request.post.firstCall.args[0] - expect(arg.json).to.have.all.keys 'docUrls', 'fullIndex' - expect(arg.json.docUrls.length).to.equal 2 - expect(arg.json.fullIndex).to.equal true - done() - - it 'should not produce an error', (done) -> - @call (err, data) => - expect(err).to.equal null - done() - - it 'should return data', (done) -> - @call (err, data) => - expect(data).to.not.equal null - expect(data).to.not.equal undefined - expect(data).to.equal @fakeResponseData - done() - - describe 'when ProjectGetter.getProject produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - it 'should not send request', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 0 - done() - - describe 'when _isFullIndex produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) - @handler._isFullIndex.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - it 'should not send request', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 0 - done() - - describe 'when flushDocToMongo produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) - @handler._isFullIndex.callsArgWith(1, false) - @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - it 'should not send request', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 0 - done() - - - describe 'when request produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) - @handler._isFullIndex.callsArgWith(1, null, false) - @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) - @request.post.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - describe 'when request responds with error status', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) - @handler._isFullIndex.callsArgWith(1, null, false) - @request.post.callsArgWith(1, null, {statusCode: 500}, null) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - describe 'indexAll', -> - - beforeEach -> - sinon.stub(@handler, '_findBibDocIds').returns(['aaa', 'ccc']) - sinon.stub(@handler, '_findBibFileIds').returns(['fff', 'ggg']) - sinon.stub(@handler, '_isFullIndex').callsArgWith(1, null, true) - @request.post.callsArgWith(1, null, {statusCode: 200}, @fakeResponseData) - @call = (callback) => - @handler.indexAll @projectId, callback - - it 'should call _findBibDocIds', (done) -> - @call (err, data) => - @handler._findBibDocIds.callCount.should.equal 1 - @handler._findBibDocIds.calledWith(@fakeProject).should.equal true - done() - - it 'should call _findBibFileIds', (done) -> - @call (err, data) => - @handler._findBibDocIds.callCount.should.equal 1 - @handler._findBibDocIds.calledWith(@fakeProject).should.equal true - done() - - it 'should call DocumentUpdaterHandler.flushDocToMongo', (done) -> - @call (err, data) => - @DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal 2 - done() - - it 'should make a request to references service', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 1 - arg = @request.post.firstCall.args[0] - expect(arg.json).to.have.all.keys 'docUrls', 'fullIndex' - expect(arg.json.docUrls.length).to.equal 4 - expect(arg.json.fullIndex).to.equal true - done() - - it 'should not produce an error', (done) -> - @call (err, data) => - expect(err).to.equal null - done() - - it 'should return data', (done) -> - @call (err, data) => - expect(data).to.not.equal null - expect(data).to.not.equal undefined - expect(data).to.equal @fakeResponseData - done() - - describe 'when ProjectGetter.getProject produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - it 'should not send request', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 0 - done() - - describe 'when _isFullIndex produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) - @handler._isFullIndex.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - it 'should not send request', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 0 - done() - - describe 'when flushDocToMongo produces an error', -> - - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) - @handler._isFullIndex.callsArgWith(1, false) - @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, data) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(data).to.equal undefined - done() - - it 'should not send request', (done) -> - @call (err, data) => - @request.post.callCount.should.equal 0 - done() - - - describe '_findBibDocIds', -> - - beforeEach -> - @fakeProject = - rootFolder: [ - docs: [ - {name: 'one.bib', _id: 'aaa'}, - {name: 'two.txt', _id: 'bbb'}, - ] - folders: [ - {docs: [{name: 'three.bib', _id: 'ccc'}], folders: []} - ] - ] - @expectedIds = ['aaa', 'ccc'] - - it 'should select the correct docIds', -> - result = @handler._findBibDocIds(@fakeProject) - expect(result).to.deep.equal @expectedIds - - it 'should not error with a non array of folders from dirty data', -> - @fakeProject.rootFolder[0].folders[0].folders = {} - result = @handler._findBibDocIds(@fakeProject) - expect(result).to.deep.equal @expectedIds - - describe '_findBibFileIds', -> - - beforeEach -> - @fakeProject = - rootFolder: [ - docs: [ - {name: 'one.bib', _id: 'aaa'}, - {name: 'two.txt', _id: 'bbb'}, - ] - fileRefs: [ - {name: 'other.bib', _id: 'ddd'} - ], - folders: [ - { - docs: [{name: 'three.bib', _id: 'ccc'}], - fileRefs: [{name: 'four.bib', _id: 'ghg'}], - folders: [] - } - ] - ] - @expectedIds = ['ddd', 'ghg'] - - it 'should select the correct docIds', -> - result = @handler._findBibFileIds(@fakeProject) - expect(result).to.deep.equal @expectedIds - - describe '_isFullIndex', -> - - beforeEach -> - @fakeProject = - owner_ref: @owner_ref = "owner-ref-123" - @owner = - features: - references: false - @UserGetter.getUser = sinon.stub() - @UserGetter.getUser.withArgs(@owner_ref, {features: true}).yields(null, @owner) - @call = (callback) => - @handler._isFullIndex @fakeProject, callback - - describe 'with references feature on', -> - - beforeEach -> - @owner.features.references = true - - it 'should return true', -> - @call (err, isFullIndex) => - expect(err).to.equal null - expect(isFullIndex).to.equal true - - describe 'with references feature off', -> - - beforeEach -> - @owner.features.references = false - - it 'should return false', -> - @call (err, isFullIndex) => - expect(err).to.equal null - expect(isFullIndex).to.equal false - - describe 'with referencesSearch', -> - - beforeEach -> - @owner.features = {referencesSearch: true, references: false} - - it 'should return true', -> - @call (err, isFullIndex) => - expect(err).to.equal null - expect(isFullIndex).to.equal true diff --git a/services/web/test/unit/coffee/Security/LoginRateLimiterTests.coffee b/services/web/test/unit/coffee/Security/LoginRateLimiterTests.coffee deleted file mode 100644 index bbb4dcd675..0000000000 --- a/services/web/test/unit/coffee/Security/LoginRateLimiterTests.coffee +++ /dev/null @@ -1,74 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -expect = require('chai').expect -modulePath = require('path').join __dirname, '../../../../app/js/Features/Security/LoginRateLimiter' - - -describe "LoginRateLimiter", -> - - beforeEach -> - @email = "bob@bob.com" - @RateLimiter = - clearRateLimit: sinon.stub() - addCount: sinon.stub() - - @LoginRateLimiter = SandboxedModule.require modulePath, requires: - '../../infrastructure/RateLimiter': @RateLimiter - - describe "processLoginRequest", -> - - beforeEach -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - - it 'should call RateLimiter.addCount', (done) -> - @LoginRateLimiter.processLoginRequest @email, (err, allow) => - @RateLimiter.addCount.callCount.should.equal 1 - expect(@RateLimiter.addCount.lastCall.args[0].endpointName).to.equal 'login' - expect(@RateLimiter.addCount.lastCall.args[0].subjectName).to.equal @email - done() - - describe 'when login is allowed', -> - - beforeEach -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - - it 'should call pass allow=true', (done) -> - @LoginRateLimiter.processLoginRequest @email, (err, allow) => - expect(err).to.equal null - expect(allow).to.equal true - done() - - describe 'when login is blocked', -> - - beforeEach -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) - - it 'should call pass allow=false', (done) -> - @LoginRateLimiter.processLoginRequest @email, (err, allow) => - expect(err).to.equal null - expect(allow).to.equal false - done() - - describe 'when addCount produces an error', -> - - beforeEach -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @LoginRateLimiter.processLoginRequest @email, (err, allow) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - - describe "recordSuccessfulLogin", -> - - beforeEach -> - @RateLimiter.clearRateLimit = sinon.stub().callsArgWith 2, null - - it "should call clearRateLimit", (done)-> - @LoginRateLimiter.recordSuccessfulLogin @email, => - @RateLimiter.clearRateLimit.callCount.should.equal 1 - @RateLimiter.clearRateLimit.calledWith('login', @email).should.equal true - done() diff --git a/services/web/test/unit/coffee/Security/OneTimeTokenHandlerTests.coffee b/services/web/test/unit/coffee/Security/OneTimeTokenHandlerTests.coffee deleted file mode 100644 index c0e6d9cc13..0000000000 --- a/services/web/test/unit/coffee/Security/OneTimeTokenHandlerTests.coffee +++ /dev/null @@ -1,103 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Security/OneTimeTokenHandler" -expect = require("chai").expect -Errors = require "../../../../app/js/Features/Errors/Errors" -tk = require("timekeeper") - -describe "OneTimeTokenHandler", -> - beforeEach -> - tk.freeze Date.now() # freeze the time for these tests - @stubbedToken = "mock-token" - @callback = sinon.stub() - @OneTimeTokenHandler = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - "crypto": randomBytes: () => @stubbedToken - "../Errors/Errors": Errors - "../../infrastructure/mongojs": db: @db = tokens: {} - - afterEach -> - tk.reset() - - describe "getNewToken", -> - beforeEach -> - @db.tokens.insert = sinon.stub().yields() - - describe 'normally', -> - beforeEach -> - @OneTimeTokenHandler.getNewToken 'password', 'mock-data-to-store', @callback - - it "should insert a generated token with a 1 hour expiry", -> - @db.tokens.insert - .calledWith({ - use: 'password' - token: @stubbedToken, - createdAt: new Date(), - expiresAt: new Date(Date.now() + 60 * 60 * 1000) - data: 'mock-data-to-store' - }) - .should.equal true - - it 'should call the callback with the token', -> - @callback.calledWith(null, @stubbedToken).should.equal true - - describe 'with an optional expiresIn parameter', -> - beforeEach -> - @OneTimeTokenHandler.getNewToken 'password', 'mock-data-to-store', { expiresIn: 42 }, @callback - - it "should insert a generated token with a custom expiry", -> - @db.tokens.insert - .calledWith({ - use: 'password' - token: @stubbedToken, - createdAt: new Date(), - expiresAt: new Date(Date.now() + 42 * 1000) - data: 'mock-data-to-store' - }) - .should.equal true - - it 'should call the callback with the token', -> - @callback.calledWith(null, @stubbedToken).should.equal true - - describe "getValueFromTokenAndExpire", -> - describe 'successfully', -> - beforeEach -> - @db.tokens.findAndModify = sinon.stub().yields(null, { data: 'mock-data' }) - @OneTimeTokenHandler.getValueFromTokenAndExpire 'password', 'mock-token', @callback - - it 'should expire the token', -> - @db.tokens.findAndModify - .calledWith({ - query: { - use: 'password' - token: 'mock-token', - expiresAt: { $gt: new Date() }, - usedAt: { $exists: false } - }, - update: { - $set: { usedAt: new Date() } - } - }) - .should.equal true - - it 'should return the data', -> - @callback.calledWith(null, 'mock-data').should.equal true - - describe 'when a valid token is not found', -> - beforeEach -> - @db.tokens.findAndModify = sinon.stub().yields(null, null) - @OneTimeTokenHandler.getValueFromTokenAndExpire 'password', 'mock-token', @callback - - it 'should return a NotFoundError', -> - @callback - .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) - .should.equal true - - - - - diff --git a/services/web/test/unit/coffee/Security/RateLimiterMiddlewareTests.coffee b/services/web/test/unit/coffee/Security/RateLimiterMiddlewareTests.coffee deleted file mode 100644 index 48bd639066..0000000000 --- a/services/web/test/unit/coffee/Security/RateLimiterMiddlewareTests.coffee +++ /dev/null @@ -1,118 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/Security/RateLimiterMiddleware' - -describe "RateLimiterMiddleware", -> - beforeEach -> - @AuthenticationController = - getLoggedInUserId: () => - @req?.session?.user?._id - @RateLimiterMiddleware = SandboxedModule.require modulePath, requires: - '../../infrastructure/RateLimiter' : @RateLimiter = {} - "logger-sharelatex": @logger = {warn: sinon.stub()} - '../Authentication/AuthenticationController': @AuthenticationController - @req = - params: {} - @res = - status: sinon.stub() - write: sinon.stub() - end: sinon.stub() - @next = sinon.stub() - - describe "rateLimit", -> - beforeEach -> - @rateLimiter = @RateLimiterMiddleware.rateLimit({ - endpointName: "test-endpoint" - params: ["project_id", "doc_id"] - timeInterval: 42 - maxRequests: 12 - }) - @req.params = { - project_id: @project_id = "project-id" - doc_id: @doc_id = "doc-id" - } - - describe "when there is no session", -> - beforeEach -> - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - @req.ip = @ip = "1.2.3.4" - @rateLimiter(@req, @res, @next) - - it "should call the rate limiter backend with the ip address", -> - @RateLimiter.addCount - .calledWith({ - endpointName: "test-endpoint" - timeInterval: 42 - throttle: 12 - subjectName: "#{@project_id}:#{@doc_id}:#{@ip}" - }) - .should.equal true - - it "should pass on to next()", -> - - - describe "when under the rate limit with logged in user", -> - beforeEach -> - @req.session = - user : - _id: @user_id = "user-id" - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - @rateLimiter(@req, @res, @next) - - it "should call the rate limiter backend with the user_id", -> - @RateLimiter.addCount - .calledWith({ - endpointName: "test-endpoint" - timeInterval: 42 - throttle: 12 - subjectName: "#{@project_id}:#{@doc_id}:#{@user_id}" - }) - .should.equal true - - it "should pass on to next()", -> - @next.called.should.equal true - - describe "when under the rate limit with anonymous user", -> - beforeEach -> - @req.ip = @ip = "1.2.3.4" - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - @rateLimiter(@req, @res, @next) - - it "should call the rate limiter backend with the ip address", -> - @RateLimiter.addCount - .calledWith({ - endpointName: "test-endpoint" - timeInterval: 42 - throttle: 12 - subjectName: "#{@project_id}:#{@doc_id}:#{@ip}" - }) - .should.equal true - - it "should pass on to next()", -> - @next.called.should.equal true - - describe "when over the rate limit", -> - beforeEach -> - @req.session = - user : - _id: @user_id = "user-id" - @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) - @rateLimiter(@req, @res, @next) - - it "should return a 429", -> - @res.status.calledWith(429).should.equal true - @res.end.called.should.equal true - - it "should not continue", -> - @next.called.should.equal false - - it "should log a warning", -> - @logger.warn - .calledWith({ - endpointName: "test-endpoint" - timeInterval: 42 - throttle: 12 - subjectName: "#{@project_id}:#{@doc_id}:#{@user_id}" - }, "rate limit exceeded") - .should.equal true diff --git a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee deleted file mode 100644 index 0b4378c5e2..0000000000 --- a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee +++ /dev/null @@ -1,199 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -expect = require('chai').expect -sinon = require 'sinon' -modulePath = "../../../../app/js/Features/Subscription/FeaturesUpdater" -assert = require("chai").assert -ObjectId = require('mongoose').Types.ObjectId - -describe "FeaturesUpdater", -> - - beforeEach -> - @user_id = ObjectId().toString() - - @FeaturesUpdater = SandboxedModule.require modulePath, requires: - './UserFeaturesUpdater': @UserFeaturesUpdater = {} - './SubscriptionLocator': @SubscriptionLocator = {} - './PlansLocator': @PlansLocator = {} - "logger-sharelatex": log:-> - 'settings-sharelatex': @Settings = {} - "../Referal/ReferalFeatures" : @ReferalFeatures = {} - "./V1SubscriptionManager": @V1SubscriptionManager = {} - '../Institutions/InstitutionsFeatures': @InstitutionsFeatures = {} - - describe "refreshFeatures", -> - beforeEach -> - @V1SubscriptionManager.notifyV1OfFeaturesChange = sinon.stub().yields() - @UserFeaturesUpdater.updateFeatures = sinon.stub().yields() - @FeaturesUpdater._getIndividualFeatures = sinon.stub().yields(null, { 'individual': 'features' }) - @FeaturesUpdater._getGroupFeatureSets = sinon.stub().yields(null, [{ 'group': 'features' }, { 'group': 'features2' }]) - @InstitutionsFeatures.getInstitutionsFeatures = sinon.stub().yields(null, { 'institutions': 'features' }) - @FeaturesUpdater._getV1Features = sinon.stub().yields(null, { 'v1': 'features' }) - @ReferalFeatures.getBonusFeatures = sinon.stub().yields(null, { 'bonus': 'features' }) - @FeaturesUpdater._mergeFeatures = sinon.stub().returns({'merged': 'features'}) - @callback = sinon.stub() - - describe "normally", -> - beforeEach -> - @FeaturesUpdater.refreshFeatures @user_id, @callback - - it "should get the individual features", -> - @FeaturesUpdater._getIndividualFeatures - .calledWith(@user_id) - .should.equal true - - it "should get the group features", -> - @FeaturesUpdater._getGroupFeatureSets - .calledWith(@user_id) - .should.equal true - - it "should get the institution features", -> - @InstitutionsFeatures.getInstitutionsFeatures - .calledWith(@user_id) - .should.equal true - - it "should get the v1 features", -> - @FeaturesUpdater._getV1Features - .calledWith(@user_id) - .should.equal true - - it "should get the bonus features", -> - @ReferalFeatures.getBonusFeatures - .calledWith(@user_id) - .should.equal true - - it "should merge from the default features", -> - @FeaturesUpdater._mergeFeatures.calledWith(@Settings.defaultFeatures).should.equal true - - it "should merge the individual features", -> - @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'individual': 'features' }).should.equal true - - it "should merge the group features", -> - @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features' }).should.equal true - @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features2' }).should.equal true - - it "should merge the institutions features", -> - @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'institutions': 'features' }).should.equal true - - it "should merge the v1 features", -> - @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'v1': 'features' }).should.equal true - - it "should merge the bonus features", -> - @FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'bonus': 'features' }).should.equal true - - it "should update the user with the merged features", -> - @UserFeaturesUpdater.updateFeatures - .calledWith(@user_id, {'merged': 'features'}) - .should.equal true - - it "should notify v1", -> - @V1SubscriptionManager.notifyV1OfFeaturesChange - .called.should.equal true - - describe "with notifyV1 == false", -> - beforeEach -> - @FeaturesUpdater.refreshFeatures @user_id, false, @callback - - it "should not notify v1", -> - @V1SubscriptionManager.notifyV1OfFeaturesChange - .called.should.equal false - - describe "_mergeFeatures", -> - it "should prefer priority over standard for compileGroup", -> - expect(@FeaturesUpdater._mergeFeatures({ - compileGroup: 'priority' - }, { - compileGroup: 'standard' - })).to.deep.equal({ - compileGroup: 'priority' - }) - expect(@FeaturesUpdater._mergeFeatures({ - compileGroup: 'standard' - }, { - compileGroup: 'priority' - })).to.deep.equal({ - compileGroup: 'priority' - }) - expect(@FeaturesUpdater._mergeFeatures({ - compileGroup: 'priority' - }, { - compileGroup: 'priority' - })).to.deep.equal({ - compileGroup: 'priority' - }) - expect(@FeaturesUpdater._mergeFeatures({ - compileGroup: 'standard' - }, { - compileGroup: 'standard' - })).to.deep.equal({ - compileGroup: 'standard' - }) - - it "should prefer -1 over any other for collaborators", -> - expect(@FeaturesUpdater._mergeFeatures({ - collaborators: -1 - }, { - collaborators: 10 - })).to.deep.equal({ - collaborators: -1 - }) - expect(@FeaturesUpdater._mergeFeatures({ - collaborators: 10 - }, { - collaborators: -1 - })).to.deep.equal({ - collaborators: -1 - }) - expect(@FeaturesUpdater._mergeFeatures({ - collaborators: 4 - }, { - collaborators: 10 - })).to.deep.equal({ - collaborators: 10 - }) - - it "should prefer the higher of compileTimeout", -> - expect(@FeaturesUpdater._mergeFeatures({ - compileTimeout: 20 - }, { - compileTimeout: 10 - })).to.deep.equal({ - compileTimeout: 20 - }) - expect(@FeaturesUpdater._mergeFeatures({ - compileTimeout: 10 - }, { - compileTimeout: 20 - })).to.deep.equal({ - compileTimeout: 20 - }) - - it "should prefer the true over false for other keys", -> - expect(@FeaturesUpdater._mergeFeatures({ - github: true - }, { - github: false - })).to.deep.equal({ - github: true - }) - expect(@FeaturesUpdater._mergeFeatures({ - github: false - }, { - github: true - })).to.deep.equal({ - github: true - }) - expect(@FeaturesUpdater._mergeFeatures({ - github: true - }, { - github: true - })).to.deep.equal({ - github: true - }) - expect(@FeaturesUpdater._mergeFeatures({ - github: false - }, { - github: false - })).to.deep.equal({ - github: false - }) diff --git a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee deleted file mode 100644 index e872930500..0000000000 --- a/services/web/test/unit/coffee/Subscription/LimitationsManagerTests.coffee +++ /dev/null @@ -1,358 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/Subscription/LimitationsManager' -Settings = require("settings-sharelatex") - -describe "LimitationsManager", -> - beforeEach -> - @project = { _id: @project_id = "project-id" } - @user = { _id: @user_id = "user-id", features:{} } - @ProjectGetter = - getProject: (project_id, fields, callback) => - if project_id == @project_id - callback null, @project - else - callback null, null - @UserGetter = - getUser: (user_id, filter, callback) => - if user_id == @user_id - callback null, @user - else - callback null, null - - @SubscriptionLocator = - getUsersSubscription: sinon.stub() - getSubscription: sinon.stub() - - @LimitationsManager = SandboxedModule.require modulePath, requires: - '../Project/ProjectGetter': @ProjectGetter - '../User/UserGetter' : @UserGetter - './SubscriptionLocator':@SubscriptionLocator - 'settings-sharelatex' : @Settings = {} - "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} - "../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} - "./V1SubscriptionManager": @V1SubscriptionManager = {} - 'logger-sharelatex':log:-> - - describe "allowedNumberOfCollaboratorsInProject", -> - describe "when the project is owned by a user without a subscription", -> - beforeEach -> - @Settings.defaultFeatures = collaborators: 23 - @project.owner_ref = @user_id - delete @user.features - @callback = sinon.stub() - @LimitationsManager.allowedNumberOfCollaboratorsInProject(@project_id, @callback) - - it "should return the default number", -> - @callback.calledWith(null, @Settings.defaultFeatures.collaborators).should.equal true - - describe "when the project is owned by a user with a subscription", -> - beforeEach -> - @project.owner_ref = @user_id - @user.features = - collaborators: 21 - @callback = sinon.stub() - @LimitationsManager.allowedNumberOfCollaboratorsInProject(@project_id, @callback) - - it "should return the number of collaborators the user is allowed", -> - @callback.calledWith(null, @user.features.collaborators).should.equal true - - describe "allowedNumberOfCollaboratorsForUser", -> - describe "when the user has no features", -> - beforeEach -> - @Settings.defaultFeatures = collaborators: 23 - delete @user.features - @callback = sinon.stub() - @LimitationsManager.allowedNumberOfCollaboratorsForUser(@user_id, @callback) - - it "should return the default number", -> - @callback.calledWith(null, @Settings.defaultFeatures.collaborators).should.equal true - - describe "when the user has features", -> - beforeEach -> - @user.features = - collaborators: 21 - @callback = sinon.stub() - @LimitationsManager.allowedNumberOfCollaboratorsForUser(@user_id, @callback) - - it "should return the number of collaborators the user is allowed", -> - @callback.calledWith(null, @user.features.collaborators).should.equal true - - describe "canAddXCollaborators", -> - describe "when the project has fewer collaborators than allowed", -> - beforeEach -> - @current_number = 1 - @allowed_number = 2 - @invite_count = 0 - @CollaboratorsHandler.getInvitedCollaboratorCount = (project_id, callback) => callback(null, @current_number) - @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) - sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => - callback(null, @allowed_number) - @callback = sinon.stub() - @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) - - it "should return true", -> - @callback.calledWith(null, true).should.equal true - - describe "when the project has fewer collaborators and invites than allowed", -> - beforeEach -> - @current_number = 1 - @allowed_number = 4 - @invite_count = 1 - @CollaboratorsHandler.getInvitedCollaboratorCount = (project_id, callback) => callback(null, @current_number) - @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) - sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => - callback(null, @allowed_number) - @callback = sinon.stub() - @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) - - it "should return true", -> - @callback.calledWith(null, true).should.equal true - - describe "when the project has fewer collaborators than allowed but I want to add more than allowed", -> - beforeEach -> - @current_number = 1 - @allowed_number = 2 - @invite_count = 0 - @CollaboratorsHandler.getInvitedCollaboratorCount = (project_id, callback) => callback(null, @current_number) - @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) - sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => - callback(null, @allowed_number) - @callback = sinon.stub() - @LimitationsManager.canAddXCollaborators(@project_id, 2, @callback) - - it "should return false", -> - @callback.calledWith(null, false).should.equal true - - describe "when the project has more collaborators than allowed", -> - beforeEach -> - @current_number = 3 - @allowed_number = 2 - @invite_count = 0 - @CollaboratorsHandler.getInvitedCollaboratorCount = (project_id, callback) => callback(null, @current_number) - @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) - sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => - callback(null, @allowed_number) - @callback = sinon.stub() - @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) - - it "should return false", -> - @callback.calledWith(null, false).should.equal true - - describe "when the project has infinite collaborators", -> - beforeEach -> - @current_number = 100 - @allowed_number = -1 - @invite_count = 0 - @CollaboratorsHandler.getInvitedCollaboratorCount = (project_id, callback) => callback(null, @current_number) - @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) - sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => - callback(null, @allowed_number) - @callback = sinon.stub() - @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) - - it "should return true", -> - @callback.calledWith(null, true).should.equal true - - describe 'when the project has more invites than allowed', -> - beforeEach -> - @current_number = 0 - @allowed_number = 2 - @invite_count = 2 - @CollaboratorsHandler.getInvitedCollaboratorCount = (project_id, callback) => callback(null, @current_number) - @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) - sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => - callback(null, @allowed_number) - @callback = sinon.stub() - @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) - - it "should return false", -> - @callback.calledWith(null, false).should.equal true - - describe 'when the project has more invites and collaborators than allowed', -> - beforeEach -> - @current_number = 1 - @allowed_number = 2 - @invite_count = 1 - @CollaboratorsHandler.getInvitedCollaboratorCount = (project_id, callback) => callback(null, @current_number) - @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count) - sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => - callback(null, @allowed_number) - @callback = sinon.stub() - @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback) - - it "should return false", -> - @callback.calledWith(null, false).should.equal true - - describe "userHasV2Subscription", -> - beforeEach -> - @SubscriptionLocator.getUsersSubscription = sinon.stub() - - it "should return true if the recurly token is set", (done)-> - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, recurlySubscription_id : "1234") - @LimitationsManager.userHasV2Subscription @user, (err, hasSubscription)-> - hasSubscription.should.equal true - done() - - it "should return false if the recurly token is not set", (done)-> - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @subscription = {} - @LimitationsManager.userHasV2Subscription @user, (err, hasSubscription)-> - hasSubscription.should.equal false - done() - - it "should return false if the subscription is undefined", (done)-> - @SubscriptionLocator.getUsersSubscription.callsArgWith(1) - @LimitationsManager.userHasV2Subscription @user, (err, hasSubscription)-> - hasSubscription.should.equal false - done() - - it "should return the subscription", (done)-> - stubbedSubscription = {freeTrial:{}, token:""} - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, stubbedSubscription) - @LimitationsManager.userHasV2Subscription @user, (err, hasSubOrIsGroupMember, subscription)-> - subscription.should.deep.equal stubbedSubscription - done() - - describe "when user has a custom account", -> - - beforeEach -> - @fakeSubscription = {customAccount: true} - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @fakeSubscription) - - it 'should return true', (done) -> - @LimitationsManager.userHasV2Subscription @user, (err, hasSubscription, subscription)-> - hasSubscription.should.equal true - done() - - it 'should return the subscription', (done) -> - @LimitationsManager.userHasV2Subscription @user, (err, hasSubscription, subscription)=> - subscription.should.deep.equal @fakeSubscription - done() - - describe "userIsMemberOfGroupSubscription", -> - beforeEach -> - @SubscriptionLocator.getMemberSubscriptions = sinon.stub() - - it "should return false if there are no groups subcriptions", (done)-> - @SubscriptionLocator.getMemberSubscriptions.callsArgWith(1, null, []) - @LimitationsManager.userIsMemberOfGroupSubscription @user, (err, isMember)-> - isMember.should.equal false - done() - - it "should return true if there are no groups subcriptions", (done)-> - @SubscriptionLocator.getMemberSubscriptions.callsArgWith(1, null, subscriptions = ["mock-subscription"]) - @LimitationsManager.userIsMemberOfGroupSubscription @user, (err, isMember, retSubscriptions)-> - isMember.should.equal true - retSubscriptions.should.equal subscriptions - done() - - describe "hasPaidSubscription", -> - beforeEach -> - @LimitationsManager.userIsMemberOfGroupSubscription = sinon.stub().yields(null, false) - @LimitationsManager.userHasV2Subscription = sinon.stub().yields(null, false) - @LimitationsManager.userHasV1Subscription = sinon.stub().yields(null, false) - - it "should return true if userIsMemberOfGroupSubscription", (done)-> - @LimitationsManager.userIsMemberOfGroupSubscription = sinon.stub().yields(null, true) - @LimitationsManager.hasPaidSubscription @user, (err, hasSubOrIsGroupMember)-> - hasSubOrIsGroupMember.should.equal true - done() - - it "should return true if userHasV2Subscription", (done)-> - @LimitationsManager.userHasV2Subscription = sinon.stub().yields(null, true) - @LimitationsManager.hasPaidSubscription @user, (err, hasSubOrIsGroupMember)-> - hasSubOrIsGroupMember.should.equal true - done() - - it "should return true if userHasV1Subscription", (done)-> - @LimitationsManager.userHasV1Subscription= sinon.stub().yields(null, true) - @LimitationsManager.hasPaidSubscription @user, (err, hasSubOrIsGroupMember)-> - hasSubOrIsGroupMember.should.equal true - done() - - it "should return false if none are true", (done)-> - @LimitationsManager.hasPaidSubscription @user, (err, hasSubOrIsGroupMember)-> - hasSubOrIsGroupMember.should.equal false - done() - - it "should have userHasSubscriptionOrIsGroupMember alias", (done)-> - @LimitationsManager.userHasSubscriptionOrIsGroupMember @user, (err, hasSubOrIsGroupMember)-> - hasSubOrIsGroupMember.should.equal false - done() - - describe "userHasV1OrV2Subscription", -> - beforeEach -> - @LimitationsManager.userHasV2Subscription = sinon.stub().yields(null, false) - @LimitationsManager.userHasV1Subscription = sinon.stub().yields(null, false) - - it "should return true if userHasV2Subscription", (done)-> - @LimitationsManager.userHasV2Subscription = sinon.stub().yields(null, true) - @LimitationsManager.userHasV1OrV2Subscription @user, (err, hasSub)-> - hasSub.should.equal true - done() - - it "should return true if userHasV1Subscription", (done)-> - @LimitationsManager.userHasV1Subscription = sinon.stub().yields(null, true) - @LimitationsManager.userHasV1OrV2Subscription @user, (err, hasSub)-> - hasSub.should.equal true - done() - - it "should return false if none are true", (done)-> - @LimitationsManager.userHasV1OrV2Subscription @user, (err, hasSub)-> - hasSub.should.equal false - done() - - describe "hasGroupMembersLimitReached", -> - - beforeEach -> - @subscriptionId = "12312" - @subscription = - membersLimit: 3 - member_ids: ["", ""] - teamInvites: [ - { email: "bob@example.com", sentAt: new Date(), token: "hey" } - ] - - it "should return true if the limit is hit (including members and invites)", (done)-> - @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) - @LimitationsManager.hasGroupMembersLimitReached @subscriptionId, (err, limitReached)-> - limitReached.should.equal true - done() - - it "should return false if the limit is not hit (including members and invites)", (done)-> - @subscription.membersLimit = 4 - @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) - @LimitationsManager.hasGroupMembersLimitReached @subscriptionId, (err, limitReached)-> - limitReached.should.equal false - done() - - it "should return true if the limit has been exceded (including members and invites)", (done)-> - @subscription.membersLimit = 2 - @SubscriptionLocator.getSubscription.callsArgWith(1, null, @subscription) - @LimitationsManager.hasGroupMembersLimitReached @subscriptionId, (err, limitReached)-> - limitReached.should.equal true - done() - - describe 'userHasV1Subscription', -> - it 'should return true if v1 returns has_subscription = true', (done) -> - @V1SubscriptionManager.getSubscriptionsFromV1 = sinon.stub().yields(null, { has_subscription: true }) - @LimitationsManager.userHasV1Subscription @user, (error, result) => - @V1SubscriptionManager.getSubscriptionsFromV1.calledWith(@user_id).should.equal true - result.should.equal true - done() - - it 'should return false if v1 returns has_subscription = false', (done) -> - @V1SubscriptionManager.getSubscriptionsFromV1 = sinon.stub().yields(null, { has_subscription: false }) - @LimitationsManager.userHasV1Subscription @user, (error, result) => - @V1SubscriptionManager.getSubscriptionsFromV1.calledWith(@user_id).should.equal true - result.should.equal false - done() - - it 'should return false if v1 returns nothing', (done) -> - @V1SubscriptionManager.getSubscriptionsFromV1 = sinon.stub().yields(null, null) - @LimitationsManager.userHasV1Subscription @user, (error, result) => - @V1SubscriptionManager.getSubscriptionsFromV1.calledWith(@user_id).should.equal true - result.should.equal false - done() diff --git a/services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee b/services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee deleted file mode 100644 index 4cfe0e162e..0000000000 --- a/services/web/test/unit/coffee/Subscription/RecurlyWrapperTests.coffee +++ /dev/null @@ -1,966 +0,0 @@ -should = require('chai').should() -expect = require('chai').expect -sinon = require 'sinon' -crypto = require 'crypto' -querystring = require 'querystring' -modulePath = "../../../../app/js/Features/Subscription/RecurlyWrapper" -SandboxedModule = require('sandboxed-module') -tk = require("timekeeper") - -fixtures = - "subscriptions/44f83d7cba354d5b84812419f923ea96": - "" + - "" + - " " + - " " + - " gold" + - " Gold plan" + - " " + - " 44f83d7cba354d5b84812419f923ea96" + - " active" + - " 800" + - " EUR" + - " 1" + - " 2011-05-27T07:00:00Z" + - " " + - " " + - " 2011-06-27T07:00:00Z" + - " 2011-07-27T07:00:00Z" + - " " + - " " + - " " + - " " + - " ipaddresses" + - " 10" + - " 150" + - " " + - " " + - " " + - " " + - " " + - "" - "recurly_js/result/70db44b10f5f4b238669480c9903f6f5": - "" + - "" + - " " + - " " + - " gold" + - " Gold plan" + - " " + - " 44f83d7cba354d5b84812419f923ea96" + - " active" + - " 800" + - " EUR" + - " 1" + - " 2011-05-27T07:00:00Z" + - " " + - " " + - " 2011-06-27T07:00:00Z" + - " 2011-07-27T07:00:00Z" + - " " + - " " + - " " + - " " + - " ipaddresses" + - " 10" + - " 150" + - " " + - " " + - " " + - " " + - " " + - "" - "accounts/104": - "" + - "" + - " " + - " " + - " " + - " " + - " " + - " " + - " 104" + - " active" + - " " + - " verena@example.com" + - " Verena" + - " Example" + - " " + - " a92468579e9c4231a6c0031c4716c01d" + - " 2011-10-25T12:00:00" + - "" - -mockApiRequest = (options, callback) -> - if fixtures[options.url] - callback(null, {statusCode : 200}, fixtures[options.url]) - else - callback("Not found", {statusCode : 404}) - - -describe "RecurlyWrapper", -> - - before -> - @settings = - plans: [{ - planCode: "collaborator" - name: "Collaborator" - features: - collaborators: -1 - versioning: true - }] - defaultPlanCode: - collaborators: 0 - versioning: false - apis: - recurly: - apiKey: 'nonsense' - privateKey: 'private_nonsense' - - tk.freeze Date.now() # freeze the time for these tests - @RecurlyWrapper = RecurlyWrapper = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "logger-sharelatex": - err: sinon.stub() - error: sinon.stub() - log: sinon.stub() - "request": sinon.stub() - - after -> - tk.reset() - - describe "getSubscription", -> - - describe "with proper subscription id", -> - before -> - @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest) - @RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) => - @recurlySubscription = recurlySubscription - after -> - @RecurlyWrapper.apiRequest.restore() - - it "should look up the subscription at the normal API end point", -> - @apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96" - - it "should return the subscription", -> - @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96" - - describe "with ReculyJS token", -> - before -> - @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest) - @RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) => - @recurlySubscription = recurlySubscription - after -> - @RecurlyWrapper.apiRequest.restore() - - it "should return the subscription", -> - @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96" - - it "should look up the subscription at the RecurlyJS API end point", -> - @apiRequest.args[0][0].url.should.equal "recurly_js/result/70db44b10f5f4b238669480c9903f6f5" - - describe "with includeAccount", -> - beforeEach -> - @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest) - @RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) => - @recurlySubscription = recurlySubscription - afterEach -> - @RecurlyWrapper.apiRequest.restore() - - it "should request the account from the API", -> - @apiRequest.args[1][0].url.should.equal "accounts/104" - - it "should populate the account attribute", -> - @recurlySubscription.account.account_code.should.equal "104" - - - describe "updateSubscription", -> - beforeEach (done) -> - @recurlySubscriptionId = "subscription-id-123" - @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) => - @requestOptions = options - callback null, {}, fixtures["subscriptions/44f83d7cba354d5b84812419f923ea96"] - @RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) => - @recurlySubscription = recurlySubscription - done() - afterEach -> - @RecurlyWrapper.apiRequest.restore() - - it "should send an update request to the API", -> - @apiRequest.called.should.equal true - @requestOptions.body.should.equal """ - - silver - now - - """ - @requestOptions.url.should.equal "subscriptions/#{@recurlySubscriptionId}" - @requestOptions.method.should.equal "put" - - it "should return the updated subscription", -> - should.exist @recurlySubscription - @recurlySubscription.plan.plan_code.should.equal "gold" - - - describe "cancelSubscription", -> - beforeEach (done) -> - @recurlySubscriptionId = "subscription-id-123" - @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) => - options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/cancel" - options.method.should.equal "put" - callback() - @RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done) - - afterEach -> - @RecurlyWrapper.apiRequest.restore() - - it "should send a cancel request to the API", -> - @apiRequest.called.should.equal true - - describe 'when the subscription is already cancelled', -> - - beforeEach -> - @RecurlyWrapper.apiRequest.restore() - @recurlySubscriptionId = "subscription-id-123" - @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) => - callback(new Error('woops'), {}, "A canceled subscription can't transition to canceled") - - it 'should not produce an error', (done) -> - @RecurlyWrapper.cancelSubscription @recurlySubscriptionId, (err) => - expect(err).to.equal null - done() - - describe "reactivateSubscription", -> - beforeEach (done) -> - @recurlySubscriptionId = "subscription-id-123" - @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) => - options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate" - options.method.should.equal "put" - callback() - @RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done) - - afterEach -> - @RecurlyWrapper.apiRequest.restore() - - it "should send a cancel request to the API", -> - @apiRequest.called.should.equal true - - - - describe "redeemCoupon", -> - - beforeEach (done) -> - @recurlyAccountId = "account-id-123" - @coupon_code = "312321312" - @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) => - options.url.should.equal "coupons/#{@coupon_code}/redeem" - options.body.indexOf("#{@recurlyAccountId}").should.not.equal -1 - options.body.indexOf("USD").should.not.equal -1 - options.method.should.equal "post" - callback() - @RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done) - - afterEach -> - @RecurlyWrapper.apiRequest.restore() - - it "should send the request to redem the coupon", -> - @apiRequest.called.should.equal true - - describe "_addressToXml", -> - - beforeEach -> - @address = - address1: "addr_one" - address2: "addr_two" - country: "some_country" - state: "some_state" - postal_code: "some_zip" - nonsenseKey: "rubbish" - - it 'should generate the correct xml', () -> - result = @RecurlyWrapper._addressToXml @address - should.equal( - result, - """ - - addr_one - addr_two - some_country - some_state - some_zip - \n - """ - ) - - describe 'createSubscription', -> - - beforeEach -> - @user = - _id: 'some_id' - email: 'user@example.com' - @subscriptionDetails = - currencyCode: "EUR" - plan_code: "some_plan_code" - coupon_code: "" - isPaypal: true - address: - address1: "addr_one" - address2: "addr_two" - country: "some_country" - state: "some_state" - zip: "some_zip" - @subscription = {} - @recurly_token_id = "a-token-id" - @call = (callback) => - @RecurlyWrapper.createSubscription(@user, @subscriptionDetails, @recurly_token_id, callback) - - - describe 'when paypal', -> - - beforeEach -> - @subscriptionDetails.isPaypal = true - @_createPaypalSubscription = sinon.stub(@RecurlyWrapper, '_createPaypalSubscription') - @_createPaypalSubscription.callsArgWith(3, null, @subscription) - - afterEach -> - @_createPaypalSubscription.restore() - - it 'should not produce an error', (done) -> - @call (err, sub) => - expect(err).to.equal null - expect(err).to.not.be.instanceof Error - done() - - it 'should produce a subscription object', (done) -> - @call (err, sub) => - expect(sub).to.deep.equal @subscription - done() - - it 'should call _createPaypalSubscription', (done) -> - @call (err, sub) => - @_createPaypalSubscription.callCount.should.equal 1 - done() - - describe "when _createPaypalSubscription produces an error", -> - - beforeEach -> - @_createPaypalSubscription.callsArgWith(3, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, sub) => - expect(err).to.be.instanceof Error - done() - - describe 'when not paypal', -> - - beforeEach -> - @subscriptionDetails.isPaypal = false - @_createCreditCardSubscription = sinon.stub(@RecurlyWrapper, '_createCreditCardSubscription') - @_createCreditCardSubscription.callsArgWith(3, null, @subscription) - - afterEach -> - @_createCreditCardSubscription.restore() - - it 'should not produce an error', (done) -> - @call (err, sub) => - expect(err).to.equal null - expect(err).to.not.be.instanceof Error - done() - - it 'should produce a subscription object', (done) -> - @call (err, sub) => - expect(sub).to.deep.equal @subscription - done() - - it 'should call _createCreditCardSubscription', (done) -> - @call (err, sub) => - @_createCreditCardSubscription.callCount.should.equal 1 - done() - - describe "when _createCreditCardSubscription produces an error", -> - - beforeEach -> - @_createCreditCardSubscription.callsArgWith(3, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, sub) => - expect(err).to.be.instanceof Error - done() - - - describe '_createCreditCardSubscription', -> - - beforeEach -> - @user = - _id: 'some_id' - email: 'user@example.com' - @subscriptionDetails = - currencyCode: "EUR" - plan_code: "some_plan_code" - coupon_code: "" - isPaypal: true - address: - address1: "addr_one" - address2: "addr_two" - country: "some_country" - state: "some_state" - zip: "some_zip" - @subscription = {} - @recurly_token_id = "a-token-id" - @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest') - @response = - statusCode: 200 - @body = "is_bad" - @apiRequest.callsArgWith(1, null, @response, @body) - @_parseSubscriptionXml = sinon.stub(@RecurlyWrapper, '_parseSubscriptionXml') - @_parseSubscriptionXml.callsArgWith(1, null, @subscription) - @call = (callback) => - @RecurlyWrapper._createCreditCardSubscription(@user, @subscriptionDetails, @recurly_token_id, callback) - - afterEach -> - @apiRequest.restore() - @_parseSubscriptionXml.restore() - - it 'should not produce an error', (done) -> - @call (err, sub) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should produce a subscription', (done) -> - @call (err, sub) => - expect(sub).to.equal @subscription - done() - - it 'should call apiRequest', (done) -> - @call (err, sub) => - @apiRequest.callCount.should.equal 1 - done() - - it 'should call _parseSubscriptionXml', (done) -> - @call (err, sub) => - @_parseSubscriptionXml.callCount.should.equal 1 - done() - - describe 'when api request produces an error', -> - - beforeEach -> - @apiRequest.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, sub) => - expect(err).to.be.instanceof Error - done() - - it 'should call apiRequest', (done) -> - @call (err, sub) => - @apiRequest.callCount.should.equal 1 - done() - - it 'should not _parseSubscriptionXml', (done) -> - @call (err, sub) => - @_parseSubscriptionXml.callCount.should.equal 0 - done() - - describe 'when parse xml produces an error', -> - - beforeEach -> - @_parseSubscriptionXml.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, sub) => - expect(err).to.be.instanceof Error - done() - - describe '_createPaypalSubscription', -> - - beforeEach -> - @checkAccountExists = sinon.stub(@RecurlyWrapper._paypal, 'checkAccountExists') - @createAccount = sinon.stub(@RecurlyWrapper._paypal, 'createAccount') - @createBillingInfo = sinon.stub(@RecurlyWrapper._paypal, 'createBillingInfo') - @setAddress = sinon.stub(@RecurlyWrapper._paypal, 'setAddress') - @createSubscription = sinon.stub(@RecurlyWrapper._paypal, 'createSubscription') - @user = - _id: 'some_id' - email: 'user@example.com' - @subscriptionDetails = - currencyCode: "EUR" - plan_code: "some_plan_code" - coupon_code: "" - isPaypal: true - address: - address1: "addr_one" - address2: "addr_two" - country: "some_country" - state: "some_state" - zip: "some_zip" - @subscription = {} - @recurly_token_id = "a-token-id" - - # set up data callbacks - user = @user - subscriptionDetails = @subscriptionDetails - recurly_token_id = @recurly_token_id - - @checkAccountExists.callsArgWith(1, null, - {user, subscriptionDetails, recurly_token_id, - userExists: false, account: {accountCode: 'xx'}} - ) - @createAccount.callsArgWith(1, null, - {user, subscriptionDetails, recurly_token_id, - userExists: false, account: {accountCode: 'xx'}} - ) - @createBillingInfo.callsArgWith(1, null, - {user, subscriptionDetails, recurly_token_id, - userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}} - ) - @setAddress.callsArgWith(1, null, - {user, subscriptionDetails, recurly_token_id, - userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}} - ) - @createSubscription.callsArgWith(1, null, - {user, subscriptionDetails, recurly_token_id, - userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}, subscription: @subscription} - ) - - @call = (callback) => - @RecurlyWrapper._createPaypalSubscription @user, @subscriptionDetails, @recurly_token_id, callback - - afterEach -> - @checkAccountExists.restore() - @createAccount.restore() - @createBillingInfo.restore() - @setAddress.restore() - @createSubscription.restore() - - it 'should not produce an error', (done) -> - @call (err, sub) => - expect(err).to.not.be.instanceof Error - done() - - it 'should produce a subscription object', (done) -> - @call (err, sub) => - expect(sub).to.not.equal null - expect(sub).to.equal @subscription - done() - - it 'should call each of the paypal stages', (done) -> - @call (err, sub) => - @checkAccountExists.callCount.should.equal 1 - @createAccount.callCount.should.equal 1 - @createBillingInfo.callCount.should.equal 1 - @setAddress.callCount.should.equal 1 - @createSubscription.callCount.should.equal 1 - done() - - describe 'when one of the paypal stages produces an error', -> - - beforeEach -> - @createAccount.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, sub) => - expect(err).to.be.instanceof Error - done() - - it 'should stop calling the paypal stages after the error', (done) -> - @call (err, sub) => - @checkAccountExists.callCount.should.equal 1 - @createAccount.callCount.should.equal 1 - @createBillingInfo.callCount.should.equal 0 - @setAddress.callCount.should.equal 0 - @createSubscription.callCount.should.equal 0 - done() - - describe 'paypal actions', -> - - beforeEach -> - @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest') - @_parseAccountXml = sinon.spy(@RecurlyWrapper, '_parseAccountXml') - @_parseBillingInfoXml = sinon.spy(@RecurlyWrapper, '_parseBillingInfoXml') - @_parseSubscriptionXml = sinon.spy(@RecurlyWrapper, '_parseSubscriptionXml') - @cache = - user: @user = {_id: 'some_id'} - recurly_token_id: @recurly_token_id = "some_token" - subscriptionDetails: @subscriptionDetails = - currencyCode: "EUR" - plan_code: "some_plan_code" - coupon_code: "" - isPaypal: true - address: - address1: "addr_one" - address2: "addr_two" - country: "some_country" - state: "some_state" - zip: "some_zip" - - afterEach -> - @apiRequest.restore() - @_parseAccountXml.restore() - @_parseBillingInfoXml.restore() - @_parseSubscriptionXml.restore() - - describe '_paypal.checkAccountExists', -> - - beforeEach -> - @call = (callback) => - @RecurlyWrapper._paypal.checkAccountExists @cache, callback - - describe 'when the account exists', -> - - beforeEach -> - resultXml = 'abc' - @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml) - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.not.be.instanceof Error - done() - - it 'should call apiRequest', (done) -> - @call (err, result) => - @apiRequest.callCount.should.equal 1 - done() - - it 'should call _parseAccountXml', (done) -> - @call (err, result) => - @RecurlyWrapper._parseAccountXml.callCount.should.equal 1 - done() - - it 'should add the account to the cumulative result', (done) -> - @call (err, result) => - expect(result.account).to.not.equal null - expect(result.account).to.not.equal undefined - expect(result.account).to.deep.equal { - account_code: 'abc' - } - done() - - it 'should set userExists to true', (done) -> - @call (err, result) => - expect(result.userExists).to.equal true - done() - - describe 'when the account does not exist', -> - - beforeEach -> - @apiRequest.callsArgWith(1, null, {statusCode: 404}, '') - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.not.be.instanceof Error - done() - - it 'should call apiRequest', (done) -> - @call (err, result) => - @apiRequest.callCount.should.equal 1 - @apiRequest.firstCall.args[0].method.should.equal 'GET' - done() - - it 'should not call _parseAccountXml', (done) -> - @call (err, result) => - @RecurlyWrapper._parseAccountXml.callCount.should.equal 0 - done() - - it 'should not add the account to result', (done) -> - @call (err, result) => - expect(result.account).to.equal undefined - done() - - it 'should set userExists to false', (done) -> - @call (err, result) => - expect(result.userExists).to.equal false - done() - - describe 'when apiRequest produces an error', -> - - beforeEach -> - @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500}) - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe '_paypal.createAccount', -> - - beforeEach -> - @call = (callback) => - @RecurlyWrapper._paypal.createAccount @cache, callback - - describe 'when address is missing from subscriptionDetails', -> - - beforeEach -> - @cache.subscriptionDetails.address = null - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe 'when account already exists', -> - - beforeEach -> - @cache.userExists = true - @cache.account = - account_code: 'abc' - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.not.be.instanceof Error - done() - - it 'should produce cache object', (done) -> - @call (err, result) => - expect(result).to.deep.equal @cache - expect(result.account).to.deep.equal { - account_code: 'abc' - } - done() - - it 'should not call apiRequest', (done) -> - @call (err, result) => - @apiRequest.callCount.should.equal 0 - done() - - it 'should not call _parseAccountXml', (done) -> - @call (err, result) => - @RecurlyWrapper._parseAccountXml.callCount.should.equal 0 - done() - - describe 'when account does not exist', -> - - beforeEach -> - @cache.userExists = false - resultXml = 'abc' - @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml) - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.not.be.instanceof Error - done() - - it 'should call apiRequest', (done) -> - @call (err, result) => - @apiRequest.callCount.should.equal 1 - @apiRequest.firstCall.args[0].method.should.equal 'POST' - done() - - it 'should call _parseAccountXml', (done) -> - @call (err, result) => - @RecurlyWrapper._parseAccountXml.callCount.should.equal 1 - done() - - describe 'when apiRequest produces an error', -> - - beforeEach -> - @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500}) - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe '_paypal.createBillingInfo', -> - - beforeEach -> - @cache.account = - account_code: 'abc' - @call = (callback) => - @RecurlyWrapper._paypal.createBillingInfo @cache, callback - - describe 'when account_code is missing from cache', -> - - beforeEach -> - @cache.account.account_code = null - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe 'when all goes well', -> - - beforeEach -> - resultXml = '1' - @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml) - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.not.be.instanceof Error - done() - - it 'should call apiRequest', (done) -> - @call (err, result) => - @apiRequest.callCount.should.equal 1 - @apiRequest.firstCall.args[0].method.should.equal 'POST' - done() - - it 'should call _parseBillingInfoXml', (done) -> - @call (err, result) => - @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1 - done() - - it 'should set billingInfo on cache', (done) -> - @call (err, result) => - expect(result.billingInfo).to.deep.equal { - a: "1" - } - done() - - describe 'when apiRequest produces an error', -> - - beforeEach -> - @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500}) - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe '_paypal.setAddress', -> - - beforeEach -> - @cache.account = - account_code: 'abc' - @cache.billingInfo = {} - @call = (callback) => - @RecurlyWrapper._paypal.setAddress @cache, callback - - describe 'when account_code is missing from cache', -> - - beforeEach -> - @cache.account.account_code = null - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe 'when address is missing from subscriptionDetails', -> - - beforeEach -> - @cache.subscriptionDetails.address = null - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe 'when all goes well', -> - - beforeEach -> - resultXml = 'London' - @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml) - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.not.be.instanceof Error - done() - - it 'should call apiRequest', (done) -> - @call (err, result) => - @apiRequest.callCount.should.equal 1 - @apiRequest.firstCall.args[0].method.should.equal 'PUT' - done() - - it 'should call _parseBillingInfoXml', (done) -> - @call (err, result) => - @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1 - done() - - it 'should set billingInfo on cache', (done) -> - @call (err, result) => - expect(result.billingInfo).to.deep.equal { - city: 'London' - } - done() - - describe 'when apiRequest produces an error', -> - - beforeEach -> - @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500}) - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe '_paypal.createSubscription', -> - - beforeEach -> - @cache.account = - account_code: 'abc' - @cache.billingInfo = {} - @call = (callback) => - @RecurlyWrapper._paypal.createSubscription @cache, callback - - describe 'when all goes well', -> - - beforeEach -> - resultXml = '1' - @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml) - - it 'should not produce an error', (done) -> - @call (err, result) => - expect(err).to.not.be.instanceof Error - done() - - it 'should call apiRequest', (done) -> - @call (err, result) => - @apiRequest.callCount.should.equal 1 - @apiRequest.firstCall.args[0].method.should.equal 'POST' - done() - - it 'should call _parseSubscriptionXml', (done) -> - @call (err, result) => - @RecurlyWrapper._parseSubscriptionXml.callCount.should.equal 1 - done() - - it 'should set subscription on cache', (done) -> - @call (err, result) => - expect(result.subscription).to.deep.equal { - a: "1" - } - done() - - describe 'when apiRequest produces an error', -> - - beforeEach -> - @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500}) - - it 'should produce an error', (done) -> - @call (err, result) => - expect(err).to.be.instanceof Error - done() - - describe "listAccountActiveSubscriptions", -> - beforeEach -> - @user_id = "mock-user-id" - @callback = sinon.stub() - @RecurlyWrapper.apiRequest = sinon.stub().yields(null, @response = {"mock": "response"}, @body = "") - @RecurlyWrapper._parseSubscriptionsXml = sinon.stub().yields(null, @subscriptions = ["mock", "subscriptions"]) - - describe "with an account", -> - beforeEach -> - @RecurlyWrapper.listAccountActiveSubscriptions @user_id, @callback - - it "should send a request to Recurly", -> - @RecurlyWrapper.apiRequest - .calledWith({ - url: "accounts/#{@user_id}/subscriptions" - qs: - state: "active" - expect404: true - }) - .should.equal true - - it "should return the subscriptions", -> - @callback.calledWith(null, @subscriptions).should.equal true - - describe "without an account", -> - beforeEach -> - @response.statusCode = 404 - @RecurlyWrapper.listAccountActiveSubscriptions @user_id, @callback - - it "should return an empty array of subscriptions", -> - @callback.calledWith(null, []).should.equal true diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee deleted file mode 100644 index 3c9d1d7385..0000000000 --- a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee +++ /dev/null @@ -1,402 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require 'sinon' -should = require("chai").should() -expect = require("chai").expect -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -modulePath = '../../../../app/js/Features/Subscription/SubscriptionController' - -mockSubscriptions = - "subscription-123-active": - uuid: "subscription-123-active" - plan: - name: "Gold" - plan_code: "gold" - current_period_ends_at: new Date() - state: "active" - unit_amount_in_cents: 999 - account: - account_code: "user-123" - -describe "SubscriptionController", -> - beforeEach -> - @user = {email:"tom@yahoo.com", _id: 'one', signUpDate: new Date('2000-10-01')} - @activeRecurlySubscription = mockSubscriptions["subscription-123-active"] - - @AuthenticationController = - getLoggedInUser: sinon.stub().callsArgWith(1, null, @user) - getLoggedInUserId: sinon.stub().returns(@user._id) - getSessionUser: sinon.stub().returns(@user) - isUserLoggedIn: sinon.stub().returns(true) - @SubscriptionHandler = - createSubscription: sinon.stub().callsArgWith(3) - updateSubscription: sinon.stub().callsArgWith(3) - reactivateSubscription: sinon.stub().callsArgWith(1) - cancelSubscription: sinon.stub().callsArgWith(1) - recurlyCallback: sinon.stub().callsArgWith(1) - startFreeTrial: sinon.stub() - - @PlansLocator = - findLocalPlanInSettings: sinon.stub() - - @LimitationsManager = - hasPaidSubscription: sinon.stub() - userHasV1OrV2Subscription : sinon.stub() - userHasV2Subscription: sinon.stub() - - @SubscriptionViewModelBuilder = - buildUsersSubscriptionViewModel:sinon.stub().callsArgWith(1, null, {}) - buildViewModel: sinon.stub() - @settings = - coupon_codes: - upgradeToAnnualPromo: - student:"STUDENTCODEHERE" - collaborator:"COLLABORATORCODEHERE" - apis: - recurly: - subdomain:"sl" - siteUrl: "http://de.sharelatex.dev:3000" - gaExperiments:{} - @GeoIpLookup = - getCurrencyCode:sinon.stub() - @UserGetter = - getUser: sinon.stub().callsArgWith(2, null, @user) - @SubscriptionController = SandboxedModule.require modulePath, requires: - '../Authentication/AuthenticationController': @AuthenticationController - './SubscriptionHandler': @SubscriptionHandler - "./PlansLocator": @PlansLocator - './SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder - "./LimitationsManager": @LimitationsManager - "../../infrastructure/GeoIpLookup":@GeoIpLookup - "logger-sharelatex": - log:-> - warn:-> - "settings-sharelatex": @settings - "../User/UserGetter": @UserGetter - "./RecurlyWrapper": @RecurlyWrapper = {} - "./FeaturesUpdater": @FeaturesUpdater = {} - "./GroupPlansData": @GroupPlansData = {} - "./V1SubscriptionManager": @V1SubscriptionManager = {} - - - @res = new MockResponse() - @req = new MockRequest() - @req.body = {} - @req.query = - planCode:"123123" - - @stubbedCurrencyCode = "GBP" - - describe "plansPage", -> - beforeEach -> - @req.ip = "1234.3123.3131.333 313.133.445.666 653.5345.5345.534" - @GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode) - - describe 'when user is logged in', (done) -> - beforeEach (done) -> - @res.callback = done - @SubscriptionController.plansPage(@req, @res) - it 'should fetch the current user', (done) -> - @UserGetter.getUser.callCount.should.equal 1 - done() - - describe 'not dependant on logged in state', (done) -> - # these could have been put in 'when user is not logged in' too - it "should set the recommended currency from the geoiplookup", (done)-> - @res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode) - @GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true - done() - it 'should include data for features table', (done) -> - @res.renderedVariables.planFeatures.length.should.not.equal 0 - done() - - describe 'when user is not logged in', (done) -> - beforeEach (done) -> - @res.callback = done - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(null) - @SubscriptionController.plansPage(@req, @res) - - it 'should not fetch the current user', (done) -> - @UserGetter.getUser.callCount.should.equal 0 - done() - - describe "paymentPage", -> - beforeEach -> - @req.headers = {} - @SubscriptionHandler.validateNoSubscriptionInRecurly = sinon.stub().yields(null, true) - @GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode) - - describe "with a user without a subscription", -> - beforeEach -> - @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false) - @PlansLocator.findLocalPlanInSettings.returns({}) - - describe "with a valid plan code", -> - - it "should render the new subscription page", (done)-> - @res.render = (page, opts)=> - page.should.equal "subscriptions/new" - done() - @SubscriptionController.paymentPage @req, @res - - describe "with a user with subscription", -> - it "should redirect to the subscription dashboard", (done)-> - @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, true) - @res.redirect = (url)=> - url.should.equal "/user/subscription?hasSubscription=true" - done() - @SubscriptionController.paymentPage(@req, @res) - - describe "with an invalid plan code", -> - it "should redirect to the subscription dashboard", (done)-> - @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false) - @PlansLocator.findLocalPlanInSettings.returns(null) - @res.redirect = (url)=> - url.should.equal "/user/subscription?hasSubscription=true" - done() - @SubscriptionController.paymentPage(@req, @res) - - describe "which currency to use", -> - beforeEach -> - @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false) - @PlansLocator.findLocalPlanInSettings.returns({}) - - it "should use the set currency from the query string", (done)-> - @req.query.currency = "EUR" - @res.render = (page, opts)=> - opts.currency.should.equal "EUR" - opts.currency.should.not.equal @stubbedCurrencyCode - done() - @SubscriptionController.paymentPage @req, @res - - it "should upercase the currency code", (done)-> - @req.query.currency = "eur" - @res.render = (page, opts)=> - opts.currency.should.equal "EUR" - done() - @SubscriptionController.paymentPage @req, @res - - - it "should use the geo ip currency if non is provided", (done)-> - @req.query.currency = null - @res.render = (page, opts)=> - opts.currency.should.equal @stubbedCurrencyCode - done() - @SubscriptionController.paymentPage @req, @res - - describe "with a recurly subscription already", -> - it "should redirect to the subscription dashboard", (done)-> - @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false) - @SubscriptionHandler.validateNoSubscriptionInRecurly = sinon.stub().yields(null, false) - @res.redirect = (url)=> - url.should.equal "/user/subscription?hasSubscription=true" - done() - @SubscriptionController.paymentPage(@req, @res) - - - describe "successful_subscription", -> - beforeEach (done) -> - @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, {}) - @res.callback = done - @SubscriptionController.successful_subscription @req, @res - - describe "userSubscriptionPage", -> - beforeEach (done) -> - @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, { - personalSubscription: @personalSubscription = { 'personal-subscription': 'mock' } - memberGroupSubscriptions: @memberGroupSubscriptions = { 'group-subscriptions': 'mock' } - }) - @SubscriptionViewModelBuilder.buildViewModel.returns(@plans = {'plans': 'mock'}) - @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false) - @res.render = (view, @data) => - expect(view).to.equal 'subscriptions/dashboard' - done() - @SubscriptionController.userSubscriptionPage @req, @res - - it "should load the personal, groups and v1 subscriptions", -> - expect(@data.personalSubscription).to.deep.equal @personalSubscription - expect(@data.memberGroupSubscriptions).to.deep.equal @memberGroupSubscriptions - - it "should load the user", -> - expect(@data.user).to.deep.equal @user - - it "should load the plans", -> - expect(@data.plans).to.deep.equal @plans - - describe "createSubscription", -> - beforeEach (done)-> - @res = - sendStatus:-> - done() - sinon.spy @res, "sendStatus" - @subscriptionDetails = - card:"1234" - cvv:"123" - @req.body.recurly_token_id = "1234" - @req.body.subscriptionDetails = @subscriptionDetails - @LimitationsManager.userHasV1OrV2Subscription.yields(null, false) - @SubscriptionController.createSubscription @req, @res - - it "should send the user and subscriptionId to the handler", (done)-> - @SubscriptionHandler.createSubscription.calledWith(@user, @subscriptionDetails, @req.body.recurly_token_id).should.equal true - done() - - it "should redurect to the subscription page", (done)-> - @res.sendStatus.calledWith(201).should.equal true - done() - - - describe "updateSubscription via post", -> - beforeEach (done)-> - @res = - redirect:-> - done() - sinon.spy @res, "redirect" - @plan_code = "1234" - @req.body.plan_code = @plan_code - @SubscriptionController.updateSubscription @req, @res - - it "should send the user and subscriptionId to the handler", (done)-> - @SubscriptionHandler.updateSubscription.calledWith(@user, @plan_code).should.equal true - done() - - it "should redurect to the subscription page", (done)-> - @res.redirect.calledWith("/user/subscription").should.equal true - done() - - describe "reactivateSubscription", -> - beforeEach (done)-> - @res = - redirect:-> - done() - sinon.spy @res, "redirect" - @SubscriptionController.reactivateSubscription @req, @res - - it "should tell the handler to reactivate this user", (done)-> - @SubscriptionHandler.reactivateSubscription.calledWith(@user).should.equal true - done() - - it "should redurect to the subscription page", (done)-> - @res.redirect.calledWith("/user/subscription").should.equal true - done() - - describe "cancelSubscription", -> - beforeEach (done)-> - @res = - redirect:-> - done() - sinon.spy @res, "redirect" - @SubscriptionController.cancelSubscription @req, @res - - it "should tell the handler to cancel this user", (done)-> - @SubscriptionHandler.cancelSubscription.calledWith(@user).should.equal true - done() - - it "should redurect to the subscription page", (done)-> - @res.redirect.calledWith("/user/subscription/canceled").should.equal true - done() - - describe "recurly callback", -> - describe "with a actionable request", -> - - beforeEach (done)-> - @req = - body: - expired_subscription_notification: - subscription: - uuid: @activeRecurlySubscription.uuid - @res = sendStatus:-> - done() - sinon.spy @res, "sendStatus" - @SubscriptionController.recurlyCallback @req, @res - - it "should tell the SubscriptionHandler to process the recurly callback", (done)-> - @SubscriptionHandler.recurlyCallback.called.should.equal true - done() - - - it "should send a 200", (done)-> - @res.sendStatus.calledWith(200) - done() - - describe "with a non-actionable request", -> - beforeEach (done) -> - @user.id = @activeRecurlySubscription.account.account_code - @req = - body: - new_subscription_notification: - subscription: - uuid: @activeRecurlySubscription.uuid - @res = sendStatus:-> - done() - sinon.spy @res, "sendStatus" - @SubscriptionController.recurlyCallback @req, @res - - it "should not call the subscriptionshandler", -> - @SubscriptionHandler.recurlyCallback.called.should.equal false - - it "should respond with a 200 status", -> - @res.sendStatus.calledWith(200) - - - describe "renderUpgradeToAnnualPlanPage", -> - - - it "should redirect to the plans page if the user does not have a subscription", (done)-> - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false) - @res.redirect = (url)-> - url.should.equal "/user/subscription/plans" - done() - @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res - - - it "should pass the plan code to the view - student", (done)-> - - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, {planCode:"Student free trial 14 days"}) - @res.render = (view, opts)-> - view.should.equal "subscriptions/upgradeToAnnual" - opts.planName.should.equal "student" - done() - @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res - - it "should pass the plan code to the view - collaborator", (done)-> - - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, {planCode:"free trial for Collaborator free trial 14 days"}) - @res.render = (view, opts)-> - opts.planName.should.equal "collaborator" - done() - @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res - - it "should pass annual as the plan name if the user is already on an annual plan", (done)-> - - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, {planCode:"student annual with free trial"}) - @res.render = (view, opts)-> - opts.planName.should.equal "annual" - done() - @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res - - - describe "processUpgradeToAnnualPlan", -> - - beforeEach -> - - it "should tell the subscription handler to update the subscription with the annual plan and apply a coupon code", (done)-> - @req.body = - planName:"student" - - @res.sendStatus = ()=> - @SubscriptionHandler.updateSubscription.calledWith(@user, "student-annual", "STUDENTCODEHERE").should.equal true - done() - - @SubscriptionController.processUpgradeToAnnualPlan @req, @res - - it "should get the collaborator coupon code", (done)-> - - @req.body = - planName:"collaborator" - - @res.sendStatus = (url)=> - @SubscriptionHandler.updateSubscription.calledWith(@user, "collaborator-annual", "COLLABORATORCODEHERE").should.equal true - done() - - @SubscriptionController.processUpgradeToAnnualPlan @req, @res diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee deleted file mode 100644 index d4ee92f4ea..0000000000 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupControllerTests.coffee +++ /dev/null @@ -1,55 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -sinon = require 'sinon' -assert = require("chai").assert -modulePath = "../../../../app/js/Features/Subscription/SubscriptionGroupController" -MockResponse = require "../helpers/MockResponse" - -describe "SubscriptionGroupController", -> - - beforeEach -> - @user = {_id:"!@312431",email:"user@email.com"} - @adminUserId = "123jlkj" - @subscriptionId = "123434325412" - @user_email = "bob@gmail.com" - @req = - session: - user: - _id: @adminUserId - email:@user_email - params: - subscriptionId:@subscriptionId - query:{} - - @subscription = { - _id: @subscriptionId - } - - @GroupHandler = - removeUserFromGroup: sinon.stub().callsArgWith(2) - - @SubscriptionLocator = - findManagedSubscription: sinon.stub().callsArgWith(1, null, @subscription) - - @AuthenticationController = - getLoggedInUserId: (req) -> req.session.user._id - getSessionUser: (req) -> req.session.user - - @Controller = SandboxedModule.require modulePath, requires: - "./SubscriptionGroupHandler":@GroupHandler - "logger-sharelatex": log:-> - "./SubscriptionLocator": @SubscriptionLocator - '../Authentication/AuthenticationController': @AuthenticationController - - - describe "removeUserFromGroup", -> - it "should use the subscription id for the logged in user and take the user id from the params", (done)-> - userIdToRemove = "31231" - @req.params = user_id: userIdToRemove - @req.entity = @subscription - - res = - send : => - @GroupHandler.removeUserFromGroup.calledWith(@subscriptionId, userIdToRemove).should.equal true - done() - @Controller.removeUserFromGroup @req, res diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee deleted file mode 100644 index 878e9b25ec..0000000000 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee +++ /dev/null @@ -1,154 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -sinon = require 'sinon' -assert = require("chai").assert -modulePath = "../../../../app/js/Features/Subscription/SubscriptionGroupHandler" - - -describe "SubscriptionGroupHandler", -> - - beforeEach -> - @adminUser_id = "12321" - @newEmail = "bob@smith.com" - @user_id = "3121321" - @email = "jim@example.com" - @user = {_id:@user_id, email:@newEmail} - @subscription_id = "31DSd1123D" - - @subscription = - admin_id: @adminUser_id - manager_ids: [@adminUser_id] - _id:@subscription_id - - @SubscriptionLocator = - getUsersSubscription: sinon.stub() - getSubscriptionByMemberIdAndId: sinon.stub() - getSubscription: sinon.stub().callsArgWith(1, null, @subscription) - - @UserCreator = - getUserOrCreateHoldingAccount: sinon.stub().callsArgWith(1, null, @user) - - @SubscriptionUpdater = - removeUserFromGroup: sinon.stub().callsArgWith(2) - getSubscription: sinon.stub().callsArgWith(2) - - @TeamInvitesHandler = - createInvite: sinon.stub().callsArgWith(2) - - @UserGetter = - getUser: sinon.stub() - getUserByAnyEmail: sinon.stub() - - @LimitationsManager = - hasGroupMembersLimitReached: sinon.stub() - - @OneTimeTokenHandler = - getValueFromTokenAndExpire:sinon.stub() - getNewToken:sinon.stub() - - @EmailHandler = - sendEmail:sinon.stub() - - @Subscription = - update: sinon.stub().yields() - findOne: sinon.stub().yields() - - @settings = - siteUrl:"http://www.sharelatex.com" - - @readStub = sinon.stub() - @NotificationsBuilder = - groupPlan: sinon.stub().returns({read:@readStub}) - - @UserMembershipViewModel = - build: (email) -> { email } - - @Handler = SandboxedModule.require modulePath, requires: - "logger-sharelatex": log:-> - "../User/UserCreator": @UserCreator - "./SubscriptionUpdater": @SubscriptionUpdater - "./SubscriptionLocator": @SubscriptionLocator - "../../models/Subscription": Subscription: @Subscription - "../User/UserGetter": @UserGetter - "./LimitationsManager": @LimitationsManager - "../Security/OneTimeTokenHandler":@OneTimeTokenHandler - "../Email/EmailHandler":@EmailHandler - "settings-sharelatex":@settings - "../Notifications/NotificationsBuilder": @NotificationsBuilder - "../UserMembership/UserMembershipViewModel": @UserMembershipViewModel - "logger-sharelatex": - err:-> - log:-> - warn:-> - - - describe "removeUserFromGroup", -> - - it "should call the subscription updater to remove the user", (done)-> - @Handler.removeUserFromGroup @adminUser_id, @user._id, (err)=> - @SubscriptionUpdater.removeUserFromGroup.calledWith(@adminUser_id, @user._id).should.equal true - done() - - describe "replaceUserReferencesInGroups", -> - beforeEach (done)-> - @oldId = "ba5eba11" - @newId = "5ca1ab1e" - @Handler.replaceUserReferencesInGroups @oldId, @newId, -> - done() - - it "replaces the admin_id", -> - @Subscription.update.calledWith( - { admin_id: @oldId }, - { admin_id: @newId } - ).should.equal true - - it "replaces the manager_ids", -> - @Subscription.update.calledWith( - {manager_ids:"ba5eba11"},{$addToSet:{manager_ids:"5ca1ab1e"}},{multi:true} - ).should.equal true - - @Subscription.update.calledWith( - {manager_ids:"ba5eba11"},{$pull:{manager_ids:"ba5eba11"}},{multi:true} - ).should.equal true - - it "replaces the member ids", -> - @Subscription.update.calledWith( - { member_ids: @oldId }, - { $addToSet: { member_ids: @newId } } - ).should.equal true - - @Subscription.update.calledWith( - { member_ids: @oldId }, - { $pull: { member_ids: @oldId } } - ).should.equal true - - describe "isUserPartOfGroup", -> - beforeEach -> - @subscription_id = "123ed13123" - - it "should return true when user is part of subscription", (done)-> - @SubscriptionLocator.getSubscriptionByMemberIdAndId.callsArgWith(2, null, {_id:@subscription_id}) - @Handler.isUserPartOfGroup @user_id, @subscription_id, (err, partOfGroup)-> - partOfGroup.should.equal true - done() - - it "should return false when no subscription is found", (done)-> - @SubscriptionLocator.getSubscriptionByMemberIdAndId.callsArgWith(2, null) - @Handler.isUserPartOfGroup @user_id, @subscription_id, (err, partOfGroup)-> - partOfGroup.should.equal false - done() - - describe "getTotalConfirmedUsersInGroup", -> - describe "for existing subscriptions", -> - beforeEach -> - @subscription.member_ids = ["12321", "3121321"] - it "should call the subscription locator and return 2 users", (done)-> - @Handler.getTotalConfirmedUsersInGroup @subscription_id, (err, count)=> - @SubscriptionLocator.getSubscription.calledWith(@subscription_id).should.equal true - count.should.equal 2 - done() - describe "for nonexistent subscriptions", -> - it "should return undefined", (done)-> - @Handler.getTotalConfirmedUsersInGroup "fake-id", (err, count)=> - should.not.exist(count) - done() diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionHandlerTests.coffee deleted file mode 100644 index d801ca760c..0000000000 --- a/services/web/test/unit/coffee/Subscription/SubscriptionHandlerTests.coffee +++ /dev/null @@ -1,260 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -sinon = require 'sinon' -querystring = require 'querystring' -modulePath = "../../../../app/js/Features/Subscription/SubscriptionHandler" - -mockRecurlySubscriptions = - "subscription-123-active": - uuid: "subscription-123-active" - plan: - name: "Gold" - plan_code: "gold" - current_period_ends_at: new Date() - state: "active" - unit_amount_in_cents: 999 - account: - account_code: "user-123" - -describe "SubscriptionHandler", -> - - beforeEach -> - @Settings = - plans : [{ - planCode: "collaborator" - name: "Collaborator" - features: - collaborators: -1 - versioning: true - }] - defaultPlanCode : - collaborators: 0 - versioning: false - @activeRecurlySubscription = mockRecurlySubscriptions["subscription-123-active"] - @User = {} - @user = - _id: @user_id = "user_id_here_" - @subscription = - recurlySubscription_id: @activeRecurlySubscription.uuid - @RecurlyWrapper = - getSubscription: sinon.stub().callsArgWith(2, null, @activeRecurlySubscription) - updateSubscription: sinon.stub().callsArgWith(2, null, @activeRecurlySubscription) - cancelSubscription: sinon.stub().callsArgWith(1) - reactivateSubscription: sinon.stub().callsArgWith(1) - redeemCoupon:sinon.stub().callsArgWith(2) - createSubscription: sinon.stub().callsArgWith(3, null, @activeRecurlySubscription) - - @DropboxHandler = - unlinkAccount:sinon.stub().callsArgWith(1) - - @SubscriptionUpdater = - syncSubscription: sinon.stub().callsArgWith(2) - startFreeTrial: sinon.stub().callsArgWith(1) - - @LimitationsManager = - userHasV2Subscription: sinon.stub() - - @EmailHandler = - sendEmail:sinon.stub() - - @AnalyticsManager = - recordEvent:sinon.stub() - - @SubscriptionHandler = SandboxedModule.require modulePath, requires: - "./RecurlyWrapper": @RecurlyWrapper - "settings-sharelatex": @Settings - '../../models/User': User:@User - './SubscriptionUpdater': @SubscriptionUpdater - "logger-sharelatex":{log:->} - './LimitationsManager':@LimitationsManager - "../Email/EmailHandler":@EmailHandler - "../Dropbox/DropboxHandler":@DropboxHandler - "../../infrastructure/Events": @Events = {emit: sinon.stub()} - "../Analytics/AnalyticsManager": @AnalyticsManager - - @SubscriptionHandler.syncSubscriptionToUser = sinon.stub().callsArgWith(2) - - - describe "createSubscription", -> - beforeEach -> - @callback = sinon.stub() - @subscriptionDetails = - cvv:"123" - number:"12345" - @recurly_token_id = "45555666" - @SubscriptionHandler.validateNoSubscriptionInRecurly = sinon.stub().yields(null, true) - - describe "successfully", -> - beforeEach -> - @SubscriptionHandler.createSubscription(@user, @subscriptionDetails, @recurly_token_id, @callback) - - it "should create the subscription with the wrapper", -> - @RecurlyWrapper.createSubscription.calledWith(@user, @subscriptionDetails, @recurly_token_id).should.equal true - - it "should sync the subscription to the user", -> - @SubscriptionUpdater.syncSubscription.calledOnce.should.equal true - @SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal @activeRecurlySubscription - @SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal @user._id - - describe "when there is already a subscription in Recurly", -> - beforeEach -> - @SubscriptionHandler.validateNoSubscriptionInRecurly = sinon.stub().yields(null, false) - @SubscriptionHandler.createSubscription(@user, @subscriptionDetails, @recurly_token_id, @callback) - - it "should return an error", -> - @callback.calledWith(new Error("user already has subscription in recurly")) - - describe "updateSubscription", -> - describe "with a user with a subscription", -> - describe "with a valid plan code", -> - beforeEach (done) -> - @plan_code = "collaborator" - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, @subscription) - @SubscriptionHandler.updateSubscription(@user, @plan_code, null, done) - - it "should update the subscription", -> - @RecurlyWrapper.updateSubscription.calledWith(@subscription.recurlySubscription_id).should.equal true - updateOptions = @RecurlyWrapper.updateSubscription.args[0][1] - updateOptions.plan_code.should.equal @plan_code - - it "should update immediately", -> - updateOptions = @RecurlyWrapper.updateSubscription.args[0][1] - updateOptions.timeframe.should.equal "now" - - it "should sync the new subscription to the user", -> - @SubscriptionUpdater.syncSubscription.calledOnce.should.equal true - @SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal @activeRecurlySubscription - @SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal @user._id - - describe "with a user without a subscription", -> - beforeEach (done) -> - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false) - @SubscriptionHandler.updateSubscription(@user, @plan_code, null, done) - - it "should redirect to the subscription dashboard", -> - @RecurlyWrapper.updateSubscription.called.should.equal false - @SubscriptionHandler.syncSubscriptionToUser.called.should.equal false - - describe "with a coupon code", -> - beforeEach (done) -> - @plan_code = "collaborator" - @coupon_code = "1231312" - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, @subscription) - @SubscriptionHandler.updateSubscription(@user, @plan_code, @coupon_code, done) - - it "should get the users account", -> - @RecurlyWrapper.getSubscription.calledWith(@activeRecurlySubscription.uuid).should.equal true - - it "should redeme the coupon", (done)-> - @RecurlyWrapper.redeemCoupon.calledWith(@activeRecurlySubscription.account.account_code, @coupon_code).should.equal true - done() - - it "should update the subscription", -> - @RecurlyWrapper.updateSubscription.calledWith(@subscription.recurlySubscription_id).should.equal true - updateOptions = @RecurlyWrapper.updateSubscription.args[0][1] - updateOptions.plan_code.should.equal @plan_code - - describe "cancelSubscription", -> - describe "with a user without a subscription", -> - beforeEach (done) -> - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false, @subscription) - @SubscriptionHandler.cancelSubscription @user, done - - - it "should redirect to the subscription dashboard", -> - @RecurlyWrapper.cancelSubscription.called.should.equal false - - describe "with a user with a subscription", -> - beforeEach (done) -> - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, @subscription) - @SubscriptionHandler.cancelSubscription @user, done - - it "should cancel the subscription", -> - @RecurlyWrapper.cancelSubscription.called.should.equal true - @RecurlyWrapper.cancelSubscription.calledWith(@subscription.recurlySubscription_id).should.equal true - - it "should trigger the cancel subscription event", -> - @Events.emit.calledWith("cancelSubscription", @user._id).should.equal true - - describe "reactiveRecurlySubscription", -> - describe "with a user without a subscription", -> - beforeEach (done) -> - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false, @subscription) - @SubscriptionHandler.reactivateSubscription @user, done - - it "should redirect to the subscription dashboard", -> - @RecurlyWrapper.reactivateSubscription.called.should.equal false - - it "should not send a notification email", -> - sinon.assert.notCalled(@EmailHandler.sendEmail) - - describe "with a user with a subscription", -> - beforeEach (done) -> - @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, @subscription) - @SubscriptionHandler.reactivateSubscription @user, done - - it "should reactivate the subscription", -> - @RecurlyWrapper.reactivateSubscription.called.should.equal true - @RecurlyWrapper.reactivateSubscription.calledWith(@subscription.recurlySubscription_id).should.equal true - - it "should send a notification email", -> - sinon.assert.calledWith(@EmailHandler.sendEmail, 'reactivatedSubscription') - - describe "recurlyCallback", -> - describe "with an actionable request", -> - beforeEach (done) -> - @user.id = @activeRecurlySubscription.account.account_code - - @User.findById = (userId, callback) => - userId.should.equal @user.id - callback null, @user - @SubscriptionHandler.recurlyCallback(@activeRecurlySubscription, done) - - it "should request the affected subscription from the API", -> - @RecurlyWrapper.getSubscription.calledWith(@activeRecurlySubscription.uuid).should.equal true - - it "should request the account details of the subscription", -> - options = @RecurlyWrapper.getSubscription.args[0][1] - options.includeAccount.should.equal true - - it "should sync the subscription to the user", -> - @SubscriptionUpdater.syncSubscription.calledOnce.should.equal true - @SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal @activeRecurlySubscription - @SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal @user._id - - describe "validateNoSubscriptionInRecurly", -> - beforeEach -> - @subscriptions = [] - @RecurlyWrapper.listAccountActiveSubscriptions = sinon.stub().yields(null, @subscriptions) - @SubscriptionUpdater.syncSubscription = sinon.stub().yields() - @callback = sinon.stub() - - describe "with no subscription in recurly", -> - beforeEach -> - @subscriptions.push @subscription = { "mock": "subscription" } - @SubscriptionHandler.validateNoSubscriptionInRecurly @user_id, @callback - - it "should call RecurlyWrapper.listAccountActiveSubscriptions with the user id", -> - @RecurlyWrapper.listAccountActiveSubscriptions - .calledWith(@user_id) - .should.equal true - - it "should sync the subscription", -> - @SubscriptionUpdater.syncSubscription - .calledWith(@subscription, @user_id) - .should.equal true - - it "should call the callback with valid == false", -> - @callback.calledWith(null, false).should.equal true - - describe "with a subscription in recurly", -> - beforeEach -> - @SubscriptionHandler.validateNoSubscriptionInRecurly @user_id, @callback - - it "should not sync the subscription", -> - @SubscriptionUpdater.syncSubscription - .called - .should.equal false - - it "should call the callback with valid == true", -> - @callback.calledWith(null, true).should.equal true diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee deleted file mode 100644 index 52dee42e84..0000000000 --- a/services/web/test/unit/coffee/Subscription/SubscriptionLocatorTests.coffee +++ /dev/null @@ -1,51 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -sinon = require 'sinon' -modulePath = "../../../../app/js/Features/Subscription/SubscriptionLocator" -assert = require("chai").assert -ObjectId = require('mongoose').Types.ObjectId - - -describe "Subscription Locator Tests", -> - - beforeEach -> - @user = - _id: "5208dd34438842e2db333333" - @subscription = {hello:"world"} - @Subscription = - findOne: sinon.stub() - find: sinon.stub() - @SubscriptionLocator = SandboxedModule.require modulePath, requires: - '../../models/Subscription': Subscription:@Subscription - "logger-sharelatex": log:-> - - describe "finding users subscription", -> - - it "should send the users features", (done)-> - @Subscription.findOne.callsArgWith(1, null, @subscription) - @SubscriptionLocator.getUsersSubscription @user, (err, subscription)=> - @Subscription.findOne.calledWith({"admin_id":@user._id}).should.equal true - subscription.should.equal @subscription - done() - - it "should error if not found", (done)-> - @Subscription.findOne.callsArgWith(1, "not found") - @SubscriptionLocator.getUsersSubscription @user, (err, subscription)=> - err.should.exist - done() - - it "should take a user id rather than the user object", (done)-> - @Subscription.findOne.callsArgWith(1, null, @subscription) - @SubscriptionLocator.getUsersSubscription @user._id, (err, subscription)=> - @Subscription.findOne.calledWith({"admin_id":@user._id}).should.equal true - subscription.should.equal @subscription - done() - - describe "finding managed subscription", -> - - it "should query the database", (done)-> - @Subscription.findOne.callsArgWith(1, null, @subscription) - @SubscriptionLocator.findManagedSubscription @user._id, (err, subscription)=> - @Subscription.findOne.calledWith({"manager_ids":@user._id}).should.equal true - subscription.should.equal @subscription - done() diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee deleted file mode 100644 index 5e7997860e..0000000000 --- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ /dev/null @@ -1,254 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -expect = require('chai').expect -sinon = require 'sinon' -modulePath = "../../../../app/js/Features/Subscription/SubscriptionUpdater" -assert = require("chai").assert -ObjectId = require('mongoose').Types.ObjectId - -describe "SubscriptionUpdater", -> - - beforeEach -> - @recurlySubscription = - uuid: "1238uoijdasjhd" - plan: - plan_code: "kjhsakjds" - @adminUser = - _id: @adminuser_id = "5208dd34438843e2db000007" - @otherUserId = "5208dd34438842e2db000005" - @allUserIds = ["13213", "dsadas", "djsaiud89"] - @userStub = _id: 'mock-user-stub-id', email: 'mock-stub-email@baz.com' - @subscription = subscription = - _id: "111111111111111111111111" - admin_id: @adminUser._id - manager_ids: [@adminUser._id] - member_ids: @allUserIds - save: sinon.stub().callsArgWith(0) - planCode:"student_or_something" - @user_id = @adminuser_id - - @groupSubscription = - _id: "222222222222222222222222" - admin_id: @adminUser._id - manager_ids: [@adminUser._id] - member_ids: @allUserIds - save: sinon.stub().callsArgWith(0) - planCode:"group_subscription" - - - @updateStub = sinon.stub().callsArgWith(2, null) - @updateManyStub = sinon.stub().callsArgWith(2, null) - @findAndModifyStub = sinon.stub().callsArgWith(2, null, @subscription) - @SubscriptionModel = class - constructor: (opts)-> - subscription.admin_id = opts.admin_id - subscription.manager_ids = [opts.admin_id] - return subscription - @remove: sinon.stub().yields() - @SubscriptionModel.update = @updateStub - @SubscriptionModel.updateMany = @updateManyStub - @SubscriptionModel.findAndModify = @findAndModifyStub - - @SubscriptionLocator = - getUsersSubscription: sinon.stub() - getGroupSubscriptionMemberOf:sinon.stub() - getMemberSubscriptions: sinon.stub().yields(null, []) - - @Settings = - defaultPlanCode: "personal" - defaultFeatures: { "default": "features" } - - @UserFeaturesUpdater = - updateFeatures : sinon.stub().yields() - - @PlansLocator = - findLocalPlanInSettings: sinon.stub().returns({}) - - @UserGetter = - getUsers: (memberIds, projection, callback) -> - users = memberIds.map (id) -> { _id: id } - callback(null, users) - getUserOrUserStubById: sinon.stub() - - @ReferalFeatures = getBonusFeatures: sinon.stub().callsArgWith(1) - @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}} - @SubscriptionUpdater = SandboxedModule.require modulePath, requires: - '../../models/Subscription': Subscription:@SubscriptionModel - './UserFeaturesUpdater': @UserFeaturesUpdater - './SubscriptionLocator': @SubscriptionLocator - '../User/UserGetter': @UserGetter - './PlansLocator': @PlansLocator - "logger-sharelatex": log:-> - 'settings-sharelatex': @Settings - "./FeaturesUpdater": @FeaturesUpdater = {} - - - describe "syncSubscription", -> - - beforeEach -> - @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) - @SubscriptionUpdater._updateSubscriptionFromRecurly = sinon.stub().callsArgWith(2) - - it "should update the subscription if the user already is admin of one", (done)-> - @SubscriptionUpdater._createNewSubscription = sinon.stub() - - @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> - @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true - @SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal true - @SubscriptionUpdater._updateSubscriptionFromRecurly.calledWith(@recurlySubscription, @subscription).should.equal true - done() - - it "should not call updateFeatures with group subscription if recurly subscription is not expired", (done)-> - @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> - @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true - @SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal true - @SubscriptionUpdater._updateSubscriptionFromRecurly.calledWith(@recurlySubscription, @subscription).should.equal true - @UserFeaturesUpdater.updateFeatures.called.should.equal false - done() - - - describe "_updateSubscriptionFromRecurly", -> - beforeEach -> - @FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) - @SubscriptionUpdater.deleteSubscription = sinon.stub().yields() - - it "should update the subscription with token etc when not expired", (done)-> - @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> - @subscription.recurlySubscription_id.should.equal @recurlySubscription.uuid - @subscription.planCode.should.equal @recurlySubscription.plan.plan_code - @subscription.save.called.should.equal true - @FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true - done() - - it "should remove the subscription when expired", (done)-> - @recurlySubscription.state = "expired" - @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> - @SubscriptionUpdater.deleteSubscription.calledWith(@subscription._id).should.equal true - done() - - it "should update all the users features", (done)-> - @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> - @FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true - @FeaturesUpdater.refreshFeatures.calledWith(@allUserIds[0]).should.equal true - @FeaturesUpdater.refreshFeatures.calledWith(@allUserIds[1]).should.equal true - @FeaturesUpdater.refreshFeatures.calledWith(@allUserIds[2]).should.equal true - done() - - it "should set group to true and save how many members can be added to group", (done)-> - @PlansLocator.findLocalPlanInSettings.withArgs(@recurlySubscription.plan.plan_code).returns({groupPlan:true, membersLimit:5}) - @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> - @subscription.membersLimit.should.equal 5 - @subscription.groupPlan.should.equal true - done() - - it "should not set group to true or set groupPlan", (done)-> - @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> - assert.notEqual @subscription.membersLimit, 5 - assert.notEqual @subscription.groupPlan, true - done() - - - describe "_createNewSubscription", -> - it "should create a new subscription then update the subscription", (done)-> - @SubscriptionUpdater._createNewSubscription @adminUser._id, => - @subscription.admin_id.should.equal @adminUser._id - @subscription.manager_ids.should.deep.equal [@adminUser._id] - @subscription.save.called.should.equal true - done() - - describe "addUserToGroup", -> - beforeEach -> - @SubscriptionUpdater.addUsersToGroup = sinon.stub().yields(null) - - it "delegates to addUsersToGroup", (done)-> - @SubscriptionUpdater.addUserToGroup @subscription._id, @otherUserId, => - @SubscriptionUpdater.addUsersToGroup - .calledWith(@subscription._id, [@otherUserId]).should.equal true - done() - - describe "addUsersToGroup", -> - beforeEach -> - @FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) - - it "should add the user ids to the group as a set", (done)-> - @SubscriptionUpdater.addUsersToGroup @subscription._id, [@otherUserId], => - searchOps = - _id: @subscription._id - insertOperation = - { $addToSet: { member_ids: { $each: [@otherUserId] } } } - @findAndModifyStub.calledWith(searchOps, insertOperation).should.equal true - done() - - it "should update the users features", (done)-> - @SubscriptionUpdater.addUserToGroup @subscription._id, @otherUserId, => - @FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true - done() - - describe "removeUserFromGroups", -> - beforeEach -> - @FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) - @UserGetter.getUserOrUserStubById.yields(null, {}, false) - @fakeSubscriptions = [{ _id: 'fake-id-1' }, { _id: 'fake-id-2' }] - @SubscriptionLocator.getMemberSubscriptions.yields(null, @fakeSubscriptions) - - it "should pull the users id from the group", (done)-> - @SubscriptionUpdater.removeUserFromGroup @subscription._id, @otherUserId, => - searchOps = - _id: @subscription._id - removeOperation = - "$pull": {member_ids:@otherUserId} - @updateManyStub.calledWith(searchOps, removeOperation).should.equal true - done() - - it "should pull the users id from all groups", (done)-> - @SubscriptionUpdater.removeUserFromAllGroups @otherUserId, => - filter = - _id: ['fake-id-1', 'fake-id-2'] - removeOperation = - "$pull": {member_ids:@otherUserId} - sinon.assert.calledWith(@updateManyStub, filter, removeOperation) - done() - - it "should update the users features", (done)-> - @SubscriptionUpdater.removeUserFromGroup @subscription._id, @otherUserId, => - @FeaturesUpdater.refreshFeatures.calledWith(@otherUserId).should.equal true - done() - - it "should not update features for user stubs", (done)-> - @UserGetter.getUserOrUserStubById.yields(null, {}, true) - @SubscriptionUpdater.removeUserFromGroup @subscription._id, @userStub._id, => - @FeaturesUpdater.refreshFeatures.called.should.equal false - done() - - describe "deleteSubscription", -> - beforeEach (done) -> - @subscription_id = ObjectId().toString() - @subscription = { - "mock": "subscription", - admin_id: ObjectId(), - member_ids: [ ObjectId(), ObjectId(), ObjectId() ] - } - @SubscriptionLocator.getSubscription = sinon.stub().yields(null, @subscription) - @FeaturesUpdater.refreshFeatures = sinon.stub().yields() - @SubscriptionUpdater.deleteSubscription @subscription_id, done - - it "should look up the subscription", -> - @SubscriptionLocator.getSubscription - .calledWith(@subscription_id) - .should.equal true - - it "should remove the subscription", -> - @SubscriptionModel.remove - .calledWith({_id: ObjectId(@subscription_id)}) - .should.equal true - - it "should downgrade the admin_id", -> - @FeaturesUpdater.refreshFeatures - .calledWith(@subscription.admin_id) - .should.equal true - - it "should downgrade all of the members", -> - for user_id in @subscription.member_ids - @FeaturesUpdater.refreshFeatures - .calledWith(user_id) - .should.equal true diff --git a/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee deleted file mode 100644 index 785b03630f..0000000000 --- a/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee +++ /dev/null @@ -1,256 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -sinon = require 'sinon' -expect = require("chai").expect -querystring = require 'querystring' -modulePath = "../../../../app/js/Features/Subscription/TeamInvitesHandler" - -ObjectId = require("mongojs").ObjectId -Errors = require("../../../../app/js/Features/Errors/Errors") - -describe "TeamInvitesHandler", -> - beforeEach -> - @manager = { - id: "666666", - first_name: "Daenerys" - last_name: "Targaryen" - email: "daenerys@example.com" - } - - @token = "aaaaaaaaaaaaaaaaaaaaaa" - - @teamInvite = { - email: "jorah@example.com", - token: @token, - } - - @subscription = { - id: "55153a8014829a865bbf700d", - _id: new ObjectId("55153a8014829a865bbf700d"), - admin_id: @manager.id, - groupPlan: true, - member_ids: [], - teamInvites: [ @teamInvite ], - save: sinon.stub().yields(null), - } - - @SubscriptionLocator = { - getUsersSubscription: sinon.stub(), - getSubscription: sinon.stub().yields(null, @subscription) - } - - @UserGetter = { - getUser: sinon.stub().yields(), - getUserByAnyEmail: sinon.stub().yields() - } - - @SubscriptionUpdater = { - addUserToGroup: sinon.stub().yields() - } - - @LimitationsManager = { - teamHasReachedMemberLimit: sinon.stub().returns(false) - } - - @Subscription = { - findOne: sinon.stub().yields() - update: sinon.stub().yields() - } - - @EmailHandler = { - sendEmail: sinon.stub().yields(null) - } - - @newToken = "bbbbbbbbb" - - @crypto = { - randomBytes: => - toString: sinon.stub().returns(@newToken) - } - - @UserGetter.getUser.withArgs(@manager.id).yields(null, @manager) - @UserGetter.getUserByAnyEmail.withArgs(@manager.email).yields(null, @manager) - - @SubscriptionLocator.getUsersSubscription.yields(null, @subscription) - @Subscription.findOne.yields(null, @subscription) - - @TeamInvitesHandler = SandboxedModule.require modulePath, requires: - "logger-sharelatex": { log: -> } - "crypto": @crypto - "settings-sharelatex": { siteUrl: "http://example.com" } - "../../models/TeamInvite": { TeamInvite: @TeamInvite = {} } - "../../models/Subscription": { Subscription: @Subscription } - "../User/UserGetter": @UserGetter - "./SubscriptionLocator": @SubscriptionLocator - "./SubscriptionUpdater": @SubscriptionUpdater - "./LimitationsManager": @LimitationsManager - "../Email/EmailHandler": @EmailHandler - "../Errors/Errors": Errors - - describe "getInvite", -> - it "returns the invite if there's one", (done) -> - @TeamInvitesHandler.getInvite @token, (err, invite, subscription) => - expect(err).to.eq(null) - expect(invite).to.deep.eq(@teamInvite) - expect(subscription).to.deep.eq(@subscription) - done() - - it "returns teamNotFound if there's none", (done) -> - @Subscription.findOne = sinon.stub().yields(null, null) - - @TeamInvitesHandler.getInvite @token, (err, invite, subscription) -> - expect(err).to.be.instanceof(Errors.NotFoundError) - done() - - describe "createInvite", -> - it "adds the team invite to the subscription", (done) -> - @TeamInvitesHandler.createInvite @manager.id, @subscription, "John.Snow@example.com", (err, invite) => - expect(err).to.eq(null) - expect(invite.token).to.eq(@newToken) - expect(invite.email).to.eq("john.snow@example.com") - expect(invite.inviterName).to.eq("Daenerys Targaryen (daenerys@example.com)") - expect(@subscription.teamInvites).to.deep.include(invite) - done() - - it "sends an email", (done) -> - @TeamInvitesHandler.createInvite @manager.id, @subscription, "John.Snow@example.com", (err, invite) => - @EmailHandler.sendEmail.calledWith("verifyEmailToJoinTeam", - sinon.match({ - to: "john.snow@example.com", - inviterName: "Daenerys Targaryen (daenerys@example.com)", - acceptInviteUrl: "http://example.com/subscription/invites/#{@newToken}/" - }) - ).should.equal true - done() - - it "refreshes the existing invite if the email has already been invited", (done) -> - originalInvite = Object.assign({}, @teamInvite) - - @TeamInvitesHandler.createInvite @manager.id, @subscription, originalInvite.email, (err, invite) => - expect(err).to.eq(null) - expect(invite).to.exist - - expect(@subscription.teamInvites.length).to.eq 1 - expect(@subscription.teamInvites).to.deep.include invite - - expect(invite.email).to.eq originalInvite.email - - @subscription.save.calledOnce.should.eq true - - done() - - it "removes any legacy invite from the subscription", (done) -> - @TeamInvitesHandler.createInvite @manager.id, @subscription, "John.Snow@example.com", (err, invite) => - @Subscription.update.calledWith( - { _id: new ObjectId("55153a8014829a865bbf700d") }, - { '$pull': { invited_emails: "john.snow@example.com" } } - ).should.eq true - done() - - describe "importInvite", -> - beforeEach -> - @sentAt = new Date() - - it "can imports an invite from v1", -> - @TeamInvitesHandler.importInvite @subscription, "A-Team", "hannibal@a-team.org", - "secret", @sentAt, (error) => - expect(error).not.to.exist - - @subscription.save.calledOnce.should.eq true - - invite = @subscription.teamInvites.find (i) -> i.email == "hannibal@a-team.org" - expect(invite.token).to.eq("secret") - expect(invite.sentAt).to.eq(@sentAt) - - - describe "acceptInvite", -> - beforeEach -> - @user = { - id: "123456789", - first_name: "Tyrion", - last_name: "Lannister", - email: "tyrion@example.com" - } - - @UserGetter.getUserByAnyEmail.withArgs(@user.email).yields(null, @user) - - @subscription.teamInvites.push({ - email: "john.snow@example.com", - token: "dddddddd", - inviterName: "Daenerys Targaryen (daenerys@example.com)" - }) - - it "adds the user to the team", (done) -> - @TeamInvitesHandler.acceptInvite "dddddddd", @user.id, => - @SubscriptionUpdater.addUserToGroup.calledWith(@subscription._id, @user.id).should.eq true - done() - - it "removes the invite from the subscription", (done) -> - @TeamInvitesHandler.acceptInvite "dddddddd", @user.id, => - @Subscription.update.calledWith( - { _id: new ObjectId("55153a8014829a865bbf700d") }, - { '$pull': { teamInvites: { email: 'john.snow@example.com' } } } - ).should.eq true - done() - - describe "revokeInvite", -> - it "removes the team invite from the subscription", (done) -> - @TeamInvitesHandler.revokeInvite @manager.id, @subscription, "jorah@example.com", => - @Subscription.update.calledWith( - { _id: new ObjectId("55153a8014829a865bbf700d") }, - { '$pull': { teamInvites: { email: "jorah@example.com" } } } - ).should.eq true - - @Subscription.update.calledWith( - { _id: new ObjectId("55153a8014829a865bbf700d") }, - { '$pull': { invited_emails: "jorah@example.com" } } - ).should.eq true - done() - - describe "createTeamInvitesForLegacyInvitedEmail", (done) -> - beforeEach -> - @subscription.invited_emails = ["eddard@example.com", "robert@example.com"] - @TeamInvitesHandler.createInvite = sinon.stub().yields(null) - @SubscriptionLocator.getGroupsWithEmailInvite = sinon.stub().yields(null, [@subscription]) - - it "sends an invitation email to addresses in the legacy invited_emails field", (done) -> - @TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail "eddard@example.com", (err, invite) => - expect(err).not.to.exist - - @TeamInvitesHandler.createInvite.calledWith( - @subscription.admin_id, - @subscription, - "eddard@example.com" - ).should.eq true - - @TeamInvitesHandler.createInvite.callCount.should.eq 1 - - done() - - describe "validation", -> - it "doesn't create an invite if the team limit has been reached", (done) -> - @LimitationsManager.teamHasReachedMemberLimit = sinon.stub().returns(true) - @TeamInvitesHandler.createInvite @manager.id, @subscription, "John.Snow@example.com", (err, invite) => - expect(err).to.deep.equal(limitReached: true) - done() - - it "doesn't create an invite if the subscription is not in a group plan", (done) -> - @subscription.groupPlan = false - @TeamInvitesHandler.createInvite @manager.id, @subscription, "John.Snow@example.com", (err, invite) => - expect(err).to.deep.equal(wrongPlan: true) - done() - - it "doesn't create an invite if the user is already part of the team", (done) -> - member = { - id: "1a2b", - _id: "1a2b", - email: "tyrion@example.com" - } - - @subscription.member_ids = [member.id] - @UserGetter.getUserByAnyEmail.withArgs(member.email).yields(null, member) - - @TeamInvitesHandler.createInvite @manager.id, @subscription, "tyrion@example.com", (err, invite) => - expect(err).to.deep.equal(alreadyInTeam: true) - expect(invite).not.to.exist - done() diff --git a/services/web/test/unit/coffee/Subscription/UserFeaturesUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/UserFeaturesUpdaterTests.coffee deleted file mode 100644 index 1ba4e0b32f..0000000000 --- a/services/web/test/unit/coffee/Subscription/UserFeaturesUpdaterTests.coffee +++ /dev/null @@ -1,23 +0,0 @@ -SandboxedModule = require('sandboxed-module') -should = require('chai').should() -sinon = require 'sinon' -modulePath = "../../../../app/js/Features/Subscription/UserFeaturesUpdater" -assert = require("chai").assert - -describe "UserFeaturesUpdater", -> - beforeEach -> - @User = - update: sinon.stub().callsArgWith(2) - @UserFeaturesUpdater = SandboxedModule.require modulePath, requires: - '../../models/User': User:@User - "logger-sharelatex": log:-> - - describe "updateFeatures", -> - it "should send the users features", (done)-> - user_id = "5208dd34438842e2db000005" - @features = {versioning:true, collaborators:10} - @UserFeaturesUpdater.updateFeatures user_id, @features, (err, features)=> - update = {"features.versioning":true, "features.collaborators":10} - @User.update.calledWith({"_id":user_id}, update).should.equal true - features.should.deep.equal @features - done() \ No newline at end of file diff --git a/services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee b/services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee deleted file mode 100644 index aefa7d1f30..0000000000 --- a/services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee +++ /dev/null @@ -1,238 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -modulePath = path.join __dirname, '../../../../app/js/Features/Subscription/V1SubscriptionManager' -sinon = require("sinon") -expect = require("chai").expect - - -describe 'V1SubscriptionManager', -> - beforeEach -> - @V1SubscriptionManager = SandboxedModule.require modulePath, requires: - "../User/UserGetter": @UserGetter = {} - "logger-sharelatex": - log: sinon.stub() - err: sinon.stub() - warn: sinon.stub() - "settings-sharelatex": @Settings = - apis: - v1: - host: @host = "http://overleaf.example.com" - v1GrandfatheredFeaturesUidCutoff: 10 - v1GrandfatheredFeatures: - github: true - mendeley: true - "request": @request = sinon.stub() - @userId = 'abcd' - @v1UserId = 42 - @user = - _id: @userId - email: 'user@example.com' - overleaf: - id: @v1UserId - - describe 'getPlanCodeFromV1', -> - beforeEach -> - @responseBody = - id: 32, - plan_name: 'pro' - @V1SubscriptionManager._v1Request = sinon.stub() - .yields(null, @responseBody) - @call = (cb) => - @V1SubscriptionManager.getPlanCodeFromV1 @userId, cb - - describe 'when all goes well', -> - it 'should call _v1Request', (done) -> - @call (err, planCode) => - expect( - @V1SubscriptionManager._v1Request.callCount - ).to.equal 1 - expect( - @V1SubscriptionManager._v1Request.calledWith( - @userId - ) - ).to.equal true - done() - - it 'should return the v1 user id', (done) -> - @call (err, planCode, v1Id) -> - expect(v1Id).to.equal @v1UserId - done() - - it 'should produce a plan-code without error', (done) -> - @call (err, planCode) => - expect(err).to.not.exist - expect(planCode).to.equal 'v1_pro' - done() - - describe 'when the plan_name from v1 is null', -> - beforeEach -> - @responseBody.plan_name = null - - it 'should produce a null plan-code without error', (done) -> - @call (err, planCode) => - expect(err).to.not.exist - expect(planCode).to.equal null - done() - - describe 'getGrandfatheredFeaturesForV1User', -> - describe 'when the user ID is greater than the cutoff', -> - it 'should return an empty feature set', (done) -> - expect(@V1SubscriptionManager.getGrandfatheredFeaturesForV1User 100).to.eql {} - done() - - describe 'when the user ID is less than the cutoff', -> - it 'should return a feature set with grandfathered properties for github and mendeley', (done) -> - expect(@V1SubscriptionManager.getGrandfatheredFeaturesForV1User 1).to.eql - github: true - mendeley: true - done() - - describe '_v1Request', -> - beforeEach -> - @UserGetter.getUser = sinon.stub() - .yields(null, @user) - - describe 'when v1IdForUser produces an error', -> - beforeEach -> - @V1SubscriptionManager.v1IdForUser = sinon.stub() - .yields(new Error('woops')) - @call = (cb) => - @V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb - - it 'should not call request', (done) -> - @call (err, planCode) => - expect( - @request.callCount - ).to.equal 0 - done() - - it 'should produce an error', (done) -> - @call (err, planCode) => - expect(err).to.exist - done() - - describe 'when v1IdForUser does not find a user', -> - beforeEach -> - @V1SubscriptionManager.v1IdForUser = sinon.stub() - .yields(null, null) - @call = (cb) => - @V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb - - it 'should not call request', (done) -> - @call (err, planCode) => - expect( - @request.callCount - ).to.equal 0 - done() - - it 'should not error', (done) -> - @call (err) => - expect(err).to.not.exist - done() - - describe 'when the request to v1 fails', -> - beforeEach -> - @request.yields(new Error('woops')) - @call = (cb) => - @V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.exist - done() - - describe 'when the call succeeds', -> - beforeEach -> - @V1SubscriptionManager.v1IdForUser = sinon.stub() - .yields(null, @v1UserId) - @request.yields(null, { statusCode: 200 }, "{}") - @call = (cb) => - @V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb - - it 'should not produce an error', (done) -> - @call (err, body, v1Id) => - expect(err).not.to.exist - done() - - it 'should return the v1 user id', (done) -> - @call (err, body, v1Id) => - expect(v1Id).to.equal @v1UserId - done() - - it 'should return the http response body', (done) -> - @call (err, body, v1Id) => - expect(body).to.equal "{}" - done() - - describe 'when the call returns an http error status code', -> - beforeEach -> - @V1SubscriptionManager.v1IdForUser = sinon.stub() - .yields(null, @v1UserId) - @request.yields(null, { statusCode: 500 }, "{}") - @call = (cb) => - @V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb - - it 'should produce an error', (done) -> - @call (err, body, v1Id) => - expect(err).to.exist - done() - - describe 'when the call returns an http not-found status code', -> - beforeEach -> - @V1SubscriptionManager.v1IdForUser = sinon.stub() - .yields(null, @v1UserId) - @request.yields(null, { statusCode: 404 }, "{}") - @call = (cb) => - @V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb - - it 'should produce an not-found error', (done) -> - @call (err, body, v1Id) => - expect(err).to.exist - expect(err.name).to.equal 'NotFoundError' - done() - - describe 'v1IdForUser', -> - beforeEach -> - @UserGetter.getUser = sinon.stub() - .yields(null, @user) - - describe 'when getUser produces an error', -> - beforeEach -> - @UserGetter.getUser = sinon.stub() - .yields(new Error('woops')) - @call = (cb) => - @V1SubscriptionManager.v1IdForUser @user_id, cb - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.exist - done() - - describe 'when getUser does not find a user', -> - beforeEach -> - @UserGetter.getUser = sinon.stub() - .yields(null, null) - @call = (cb) => - @V1SubscriptionManager.v1IdForUser @user_id, cb - - it 'should not error', (done) -> - @call (err, user_id) => - expect(err).to.not.exist - done() - - describe 'when it works', -> - beforeEach -> - @call = (cb) => - @V1SubscriptionManager.v1IdForUser @user_id, cb - - it 'should not error', (done) -> - @call (err, user_id) => - expect(err).to.not.exist - done() - - it 'should return the v1 user id', (done) -> - @call (err, user_id) => - expect(user_id).to.eql 42 - done() diff --git a/services/web/test/unit/coffee/SudoMode/SudoModeControllerTests.coffee b/services/web/test/unit/coffee/SudoMode/SudoModeControllerTests.coffee deleted file mode 100644 index 2532a40898..0000000000 --- a/services/web/test/unit/coffee/SudoMode/SudoModeControllerTests.coffee +++ /dev/null @@ -1,296 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require 'sinon' -should = require("chai").should() -expect = require('chai').expect -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -modulePath = '../../../../app/js/Features/SudoMode/SudoModeController' - -describe 'SudoModeController', -> - beforeEach -> - @user = - _id: 'abcd' - email: 'user@example.com' - @UserGetter = - getUser: sinon.stub().callsArgWith(2, null, @user) - @SudoModeHandler = - authenticate: sinon.stub() - isSudoModeActive: sinon.stub() - activateSudoMode: sinon.stub() - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user._id) - _getRediretFromSession: sinon.stub() - @UserGetter = - getUser: sinon.stub() - @SudoModeController = SandboxedModule.require modulePath, requires: - 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} - './SudoModeHandler': @SudoModeHandler - '../Authentication/AuthenticationController': @AuthenticationController - '../../infrastructure/Mongoose': {mongo: {ObjectId: () -> 'some_object_id'}} - '../User/UserGetter': @UserGetter - 'settings-sharelatex': @Settings = {} - - describe 'sudoModePrompt', -> - beforeEach -> - @SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, false) - @req = {externalAuthenticationSystemUsed: sinon.stub().returns(false)} - @res = {redirect: sinon.stub(), render: sinon.stub()} - @next = sinon.stub() - - it 'should get the logged in user id', -> - @SudoModeController.sudoModePrompt(@req, @res, @next) - @AuthenticationController.getLoggedInUserId.callCount.should.equal 1 - @AuthenticationController.getLoggedInUserId.calledWith(@req).should.equal true - - it 'should check if sudo-mode is active', -> - @SudoModeController.sudoModePrompt(@req, @res, @next) - @SudoModeHandler.isSudoModeActive.callCount.should.equal 1 - @SudoModeHandler.isSudoModeActive.calledWith(@user._id).should.equal true - - it 'should redirect when sudo-mode is active', -> - @SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, true) - @SudoModeController.sudoModePrompt(@req, @res, @next) - @res.redirect.callCount.should.equal 1 - @res.redirect.calledWith('/project').should.equal true - - it 'should render the sudo_mode_prompt page when sudo mode is not active', -> - @SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, false) - @SudoModeController.sudoModePrompt(@req, @res, @next) - @res.render.callCount.should.equal 1 - @res.render.calledWith('sudo_mode/sudo_mode_prompt').should.equal true - - describe 'when isSudoModeActive produces an error', -> - beforeEach -> - @SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, new Error('woops')) - @next = sinon.stub() - - it 'should call next with an error', -> - @SudoModeController.sudoModePrompt(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should not render page', -> - @SudoModeController.sudoModePrompt(@req, @res, @next) - @res.render.callCount.should.equal 0 - - describe 'when external auth system is used', -> - beforeEach -> - @req.externalAuthenticationSystemUsed = sinon.stub().returns(true) - - it 'should redirect', -> - @SudoModeController.sudoModePrompt(@req, @res, @next) - @res.redirect.callCount.should.equal 1 - @res.redirect.calledWith('/project').should.equal true - - it 'should not check if sudo mode is active', -> - @SudoModeController.sudoModePrompt(@req, @res, @next) - @SudoModeHandler.isSudoModeActive.callCount.should.equal 0 - - it 'should not render page', -> - @SudoModeController.sudoModePrompt(@req, @res, @next) - @res.render.callCount.should.equal 0 - - describe 'submitPassword', -> - beforeEach -> - @AuthenticationController._getRedirectFromSession = sinon.stub().returns '/somewhere' - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) - @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, null, @user) - @SudoModeHandler.activateSudoMode = sinon.stub().callsArgWith(1, null) - @password = 'a_terrible_secret' - @req = {body: {password: @password}} - @res = {json: sinon.stub()} - @next = sinon.stub() - - describe 'when all goes well', -> - beforeEach -> - - it 'should get the logged in user id', -> - @SudoModeController.submitPassword(@req, @res, @next) - @AuthenticationController.getLoggedInUserId.callCount.should.equal 1 - @AuthenticationController.getLoggedInUserId.calledWith(@req).should.equal true - - it 'should get redirect from session', -> - @SudoModeController.submitPassword(@req, @res, @next) - @AuthenticationController._getRedirectFromSession.callCount.should.equal 1 - @AuthenticationController._getRedirectFromSession.calledWith(@req).should.equal true - - it 'should get the user from storage', -> - @SudoModeController.submitPassword(@req, @res, @next) - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true - - it 'should try to authenticate the user with the password', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.authenticate.callCount.should.equal 1 - @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true - - it 'should activate sudo mode', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.activateSudoMode.callCount.should.equal 1 - @SudoModeHandler.activateSudoMode.calledWith(@user._id).should.equal true - - it 'should send back a json response', -> - @SudoModeController.submitPassword(@req, @res, @next) - @res.json.callCount.should.equal 1 - @res.json.calledWith({redir: '/somewhere'}).should.equal true - - it 'should not call next', -> - @SudoModeController.submitPassword(@req, @res, @next) - @next.callCount.should.equal 0 - - describe 'when no password is supplied', -> - beforeEach -> - @req.body.password = '' - @next = sinon.stub() - - it 'should return next with an error', -> - @SudoModeController.submitPassword(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should not get the user from storage', -> - @SudoModeController.submitPassword(@req, @res, @next) - @UserGetter.getUser.callCount.should.equal 0 - - it 'should not try to authenticate the user with the password', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.authenticate.callCount.should.equal 0 - - it 'should not activate sudo mode', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.activateSudoMode.callCount.should.equal 0 - - it 'should not send back a json response', -> - @SudoModeController.submitPassword(@req, @res, @next) - @res.json.callCount.should.equal 0 - - describe 'when getUser produces an error', -> - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) - @next = sinon.stub() - - it 'should return next with an error', -> - @SudoModeController.submitPassword(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should get the user from storage', -> - @SudoModeController.submitPassword(@req, @res, @next) - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true - - it 'should not try to authenticate the user with the password', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.authenticate.callCount.should.equal 0 - - it 'should not activate sudo mode', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.activateSudoMode.callCount.should.equal 0 - - it 'should not send back a json response', -> - @SudoModeController.submitPassword(@req, @res, @next) - @res.json.callCount.should.equal 0 - - describe 'when getUser does not find a user', -> - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) - @next = sinon.stub() - - it 'should return next with an error', -> - @SudoModeController.submitPassword(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should get the user from storage', -> - @SudoModeController.submitPassword(@req, @res, @next) - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true - - it 'should not try to authenticate the user with the password', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.authenticate.callCount.should.equal 0 - - it 'should not activate sudo mode', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.activateSudoMode.callCount.should.equal 0 - - it 'should not send back a json response', -> - @SudoModeController.submitPassword(@req, @res, @next) - @res.json.callCount.should.equal 0 - - describe 'when authentication fails', -> - beforeEach -> - @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, null, null) - @res.json = sinon.stub() - @req.i18n = {translate: sinon.stub()} - - it 'should send back a failure message', -> - @SudoModeController.submitPassword(@req, @res, @next) - @res.json.callCount.should.equal 1 - expect(@res.json.lastCall.args[0]).to.have.keys ['message'] - expect(@res.json.lastCall.args[0].message).to.have.keys ['text', 'type'] - @req.i18n.translate.callCount.should.equal 1 - @req.i18n.translate.calledWith('invalid_password') - - it 'should get the user from storage', -> - @SudoModeController.submitPassword(@req, @res, @next) - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true - - it 'should try to authenticate the user with the password', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.authenticate.callCount.should.equal 1 - @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true - - it 'should not activate sudo mode', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.activateSudoMode.callCount.should.equal 0 - - describe 'when authentication produces an error', -> - beforeEach -> - @SudoModeHandler.authenticate = sinon.stub().callsArgWith(2, new Error('woops')) - @next = sinon.stub() - - it 'should return next with an error', -> - @SudoModeController.submitPassword(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should get the user from storage', -> - @SudoModeController.submitPassword(@req, @res, @next) - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true - - it 'should try to authenticate the user with the password', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.authenticate.callCount.should.equal 1 - @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true - - it 'should not activate sudo mode', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.activateSudoMode.callCount.should.equal 0 - - describe 'when sudo mode activation produces an error', -> - beforeEach -> - @SudoModeHandler.activateSudoMode = sinon.stub().callsArgWith(1, new Error('woops')) - @next = sinon.stub() - - it 'should return next with an error', -> - @SudoModeController.submitPassword(@req, @res, @next) - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - - it 'should get the user from storage', -> - @SudoModeController.submitPassword(@req, @res, @next) - @UserGetter.getUser.callCount.should.equal 1 - @UserGetter.getUser.calledWith('some_object_id', {email: 1}).should.equal true - - it 'should try to authenticate the user with the password', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.authenticate.callCount.should.equal 1 - @SudoModeHandler.authenticate.calledWith(@user.email, @password).should.equal true - - it 'should have tried to activate sudo mode', -> - @SudoModeController.submitPassword(@req, @res, @next) - @SudoModeHandler.activateSudoMode.callCount.should.equal 1 - @SudoModeHandler.activateSudoMode.calledWith(@user._id).should.equal true diff --git a/services/web/test/unit/coffee/SudoMode/SudoModeHandlerTests.coffee b/services/web/test/unit/coffee/SudoMode/SudoModeHandlerTests.coffee deleted file mode 100644 index e23797087f..0000000000 --- a/services/web/test/unit/coffee/SudoMode/SudoModeHandlerTests.coffee +++ /dev/null @@ -1,208 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -expect = require('chai').expect -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/SudoMode/SudoModeHandler' - - -describe 'SudoModeHandler', -> - beforeEach -> - @userId = 'some_user_id' - @email = 'someuser@example.com' - @user = - _id: @userId - email: @email - @rclient = {get: sinon.stub(), set: sinon.stub(), del: sinon.stub()} - @RedisWrapper = - client: () => @rclient - @SudoModeHandler = SandboxedModule.require modulePath, requires: - '../../infrastructure/RedisWrapper': @RedisWrapper - 'logger-sharelatex': @logger = {log: sinon.stub(), err: sinon.stub()} - '../Authentication/AuthenticationManager': @AuthenticationManager = {} - 'settings-sharelatex': @Settings = {} - '../V1/V1Handler': @V1Handler = {authWithV1: sinon.stub()} - '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} - - describe '_buildKey', -> - - it 'should build a properly formed key', -> - expect(@SudoModeHandler._buildKey('123')).to.equal 'SudoMode:{123}' - - describe 'activateSudoMode', -> - beforeEach -> - @call = (cb) => - @SudoModeHandler.activateSudoMode @userId, cb - - describe 'when all goes well', -> - beforeEach -> - @rclient.set = sinon.stub().callsArgWith(4, null) - - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.equal null - done() - - it 'should set a value in redis', (done) -> - @call (err) => - expect(@rclient.set.callCount).to.equal 1 - expect(@rclient.set.calledWith( - 'SudoMode:{some_user_id}', '1', 'EX', 60*60 - )).to.equal true - done() - - describe 'when user id is not supplied', -> - beforeEach -> - @call = (cb) => - @SudoModeHandler.activateSudoMode null, cb - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - it 'should not set value in redis', (done) -> - @call (err) => - expect(@rclient.set.callCount).to.equal 0 - done() - - describe 'when rclient.set produces an error', -> - beforeEach -> - @rclient.set = sinon.stub().callsArgWith(4, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - describe 'clearSudoMode', -> - beforeEach -> - @rclient.del = sinon.stub().callsArgWith(1, null) - @call = (cb) => - @SudoModeHandler.clearSudoMode @userId, cb - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.equal null - done() - - it 'should delete key from redis', (done) -> - @call (err) => - expect(@rclient.del.callCount).to.equal 1 - expect(@rclient.del.calledWith( - 'SudoMode:{some_user_id}' - )).to.equal true - done() - - describe 'when rclient.del produces an error', -> - beforeEach -> - @rclient.del = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - describe 'when user id is not supplied', -> - beforeEach -> - @call = (cb) => - @SudoModeHandler.clearSudoMode null, cb - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - it 'should not delete value in redis', (done) -> - @call (err) => - expect(@rclient.del.callCount).to.equal 0 - done() - - describe 'authenticate', -> - beforeEach -> - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) - - it 'should call AuthenticationManager.authenticate', (done) -> - @SudoModeHandler.authenticate @email, 'password', (err, user) => - expect(err).to.not.exist - expect(user).to.exist - expect(user).to.deep.equal @user - expect(@AuthenticationManager.authenticate.callCount).to.equal 1 - done() - - describe 'isSudoModeActive', -> - beforeEach -> - @call = (cb) => - @SudoModeHandler.isSudoModeActive @userId, cb - - describe 'when sudo-mode is active for that user', -> - beforeEach -> - @rclient.get = sinon.stub().callsArgWith(1, null, '1') - - it 'should not produce an error', (done) -> - @call (err, isActive) => - expect(err).to.equal null - done() - - it 'should get the value from redis', (done) -> - @call (err, isActive) => - expect(@rclient.get.callCount).to.equal 1 - expect(@rclient.get.calledWith('SudoMode:{some_user_id}')).to.equal true - done() - - it 'should produce a true result', (done) -> - @call (err, isActive) => - expect(isActive).to.equal true - done() - - describe 'when sudo-mode is not active for that user', -> - beforeEach -> - @rclient.get = sinon.stub().callsArgWith(1, null, null) - - it 'should not produce an error', (done) -> - @call (err, isActive) => - expect(err).to.equal null - done() - - it 'should get the value from redis', (done) -> - @call (err, isActive) => - expect(@rclient.get.callCount).to.equal 1 - expect(@rclient.get.calledWith('SudoMode:{some_user_id}')).to.equal true - done() - - it 'should produce a false result', (done) -> - @call (err, isActive) => - expect(isActive).to.equal false - done() - - describe 'when rclient.get produces an error', -> - beforeEach -> - @rclient.get = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, isActive) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - expect(isActive).to.be.oneOf [null, undefined] - done() - - describe 'when user id is not supplied', -> - beforeEach -> - @call = (cb) => - @SudoModeHandler.isSudoModeActive null, cb - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - it 'should not get value in redis', (done) -> - @call (err) => - expect(@rclient.get.callCount).to.equal 0 - done() diff --git a/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewareTests.coffee b/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewareTests.coffee deleted file mode 100644 index 3c2ce1a5be..0000000000 --- a/services/web/test/unit/coffee/SudoMode/SudoModeMiddlewareTests.coffee +++ /dev/null @@ -1,130 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -expect = require('chai').expect -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/SudoMode/SudoModeMiddleware' - - -describe 'SudoModeMiddleware', -> - beforeEach -> - @userId = 'some_user_id' - @SudoModeHandler = - isSudoModeActive: sinon.stub() - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@userId) - setRedirectInSession: sinon.stub() - @SudoModeMiddleware = SandboxedModule.require modulePath, requires: - './SudoModeHandler': @SudoModeHandler - '../Authentication/AuthenticationController': @AuthenticationController - 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} - 'settings-sharelatex': @Settings = {} - - describe 'protectPage', -> - beforeEach -> - @externalAuth = false - @call = (cb) => - @req = {externalAuthenticationSystemUsed: sinon.stub().returns(@externalAuth)} - @res = {redirect: sinon.stub()} - @next = sinon.stub() - @SudoModeMiddleware.protectPage @req, @res, @next - cb() - - describe 'when sudo mode is active', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId) - @SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, true) - - it 'should get the current user id', (done) -> - @call () => - @AuthenticationController.getLoggedInUserId.callCount.should.equal 1 - done() - - it 'should check if sudo-mode is active', (done) -> - @call () => - @SudoModeHandler.isSudoModeActive.callCount.should.equal 1 - @SudoModeHandler.isSudoModeActive.calledWith(@userId).should.equal true - done() - - it 'should call next', (done) -> - @call () => - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.equal undefined - done() - - describe 'when sudo mode is not active', -> - beforeEach -> - @AuthenticationController.setRedirectInSession = sinon.stub() - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId) - @SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, null, false) - - it 'should get the current user id', (done) -> - @call () => - @AuthenticationController.getLoggedInUserId.callCount.should.equal 1 - done() - - it 'should check if sudo-mode is active', (done) -> - @call () => - @SudoModeHandler.isSudoModeActive.callCount.should.equal 1 - @SudoModeHandler.isSudoModeActive.calledWith(@userId).should.equal true - done() - - it 'should set redirect in session', (done) -> - @call () => - @AuthenticationController.setRedirectInSession.callCount.should.equal 1 - @AuthenticationController.setRedirectInSession.calledWith(@req).should.equal true - done() - - it 'should redirect to the password-prompt page', (done) -> - @call () => - @res.redirect.callCount.should.equal 1 - @res.redirect.calledWith('/confirm-password').should.equal true - done() - - describe 'when isSudoModeActive produces an error', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId) - @SudoModeHandler.isSudoModeActive = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should get the current user id', (done) -> - @call () => - @AuthenticationController.getLoggedInUserId.callCount.should.equal 1 - done() - - it 'should check if sudo-mode is active', (done) -> - @call () => - @SudoModeHandler.isSudoModeActive.callCount.should.equal 1 - @SudoModeHandler.isSudoModeActive.calledWith(@userId).should.equal true - done() - - it 'should call next with an error', (done) -> - @call () => - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - describe 'when external auth is being used', -> - beforeEach -> - @externalAuth = true - @call = (cb) => - @req = {externalAuthenticationSystemUsed: sinon.stub().returns(@externalAuth)} - @res = {redirect: sinon.stub()} - @next = sinon.stub() - @SudoModeMiddleware.protectPage @req, @res, @next - cb() - - it 'should immediately return next with no args', (done) -> - @call () => - @next.callCount.should.equal 1 - expect(@next.lastCall.args[0]).to.not.exist - done() - - it 'should not get the current user id', (done) -> - @call () => - @AuthenticationController.getLoggedInUserId.callCount.should.equal 0 - done() - - it 'should not check if sudo-mode is active', (done) -> - @call () => - @SudoModeHandler.isSudoModeActive.callCount.should.equal 0 - done() diff --git a/services/web/test/unit/coffee/SystemMessages/SystemMessageManagerTests.coffee b/services/web/test/unit/coffee/SystemMessages/SystemMessageManagerTests.coffee deleted file mode 100644 index 56a0b0e2a0..0000000000 --- a/services/web/test/unit/coffee/SystemMessages/SystemMessageManagerTests.coffee +++ /dev/null @@ -1,59 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/SystemMessages/SystemMessageManager.js' - - -describe 'SystemMessageManager', -> - beforeEach -> - @SystemMessage = {} - @SystemMessageManager = SandboxedModule.require modulePath, requires: - "../../models/SystemMessage": SystemMessage: @SystemMessage - @callback = sinon.stub() - - describe "getMessage", -> - beforeEach -> - @messages = ["messages-stub"] - @SystemMessage.find = sinon.stub().callsArgWith(1, null, @messages) - - describe "when the messages are not cached", -> - beforeEach -> - @SystemMessageManager.getMessages @callback - - it "should look the messages up in the database", -> - @SystemMessage.find - .calledWith({}) - .should.equal true - - it "should return the messages", -> - @callback.calledWith(null, @messages).should.equal true - - it "should cache the messages", -> - @SystemMessageManager._cachedMessages.should.equal @messages - - describe "when the messages are cached", -> - beforeEach -> - @SystemMessageManager._cachedMessages = @messages - @SystemMessageManager.getMessages @callback - - it "should not look the messages up in the database", -> - @SystemMessage.find.called.should.equal false - - it "should return the messages", -> - @callback.calledWith(null, @messages).should.equal true - - describe "clearMessages", -> - beforeEach -> - @SystemMessage.remove = sinon.stub().callsArg(1) - @SystemMessageManager.clearMessages @callback - - it "should remove the messages from the database", -> - @SystemMessage.remove - .calledWith({}) - .should.equal true - - it "should return the callback", -> - @callback.called.should.equal true - - diff --git a/services/web/test/unit/coffee/Tags/TagsControllerTests.coffee b/services/web/test/unit/coffee/Tags/TagsControllerTests.coffee deleted file mode 100644 index 059460a4c4..0000000000 --- a/services/web/test/unit/coffee/Tags/TagsControllerTests.coffee +++ /dev/null @@ -1,140 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Tags/TagsController.js' - - -describe 'TagsController', -> - user_id = "123nd3ijdks" - project_id = "123njdskj9jlk" - tag = "some_class101" - - beforeEach -> - @handler = - addProjectToTag: sinon.stub().callsArgWith(3) - removeProjectFromTag: sinon.stub().callsArgWith(3) - deleteTag: sinon.stub().callsArg(2) - renameTag: sinon.stub().callsArg(3) - createTag: sinon.stub() - @AuthenticationController = - getLoggedInUserId: (req) => - req.session.user._id - @controller = SandboxedModule.require modulePath, requires: - "./TagsHandler":@handler - 'logger-sharelatex': - log:-> - err:-> - '../Authentication/AuthenticationController': @AuthenticationController - @req = - params: - project_id:project_id - session: - user: - _id:user_id - - @res = {} - @res.status = sinon.stub().returns @res - @res.end = sinon.stub() - @res.json = sinon.stub() - - describe "getAllTags", -> - it 'should ask the handler for all tags', (done)-> - allTags = [{name:"tag", projects:["123423","423423"]}] - @handler.getAllTags = sinon.stub().callsArgWith(1, null, allTags) - @controller.getAllTags @req, json:(body)=> - body.should.equal allTags - @handler.getAllTags.calledWith(user_id).should.equal true - done() - - describe "createTag", -> - beforeEach -> - @handler.createTag.callsArgWith(2, null, @tag = {"mock": "tag"}) - @req.session.user._id = @user_id = "user-id-123" - @req.body = name: @name = "tag-name" - @controller.createTag @req, @res - - it "should create the tag in the backend", -> - @handler.createTag - .calledWith(@user_id, @name) - .should.equal true - - it "should return the tag", -> - @res.json.calledWith(@tag).should.equal true - - describe "deleteTag", -> - beforeEach -> - @req.params.tag_id = @tag_id = "tag-id-123" - @req.session.user._id = @user_id = "user-id-123" - @controller.deleteTag @req, @res - - it "should delete the tag in the backend", -> - @handler.deleteTag - .calledWith(@user_id, @tag_id) - .should.equal true - - it "should return 204 status code", -> - @res.status.calledWith(204).should.equal true - @res.end.called.should.equal true - - describe "renameTag", -> - beforeEach -> - @req.params.tag_id = @tag_id = "tag-id-123" - @req.session.user._id = @user_id = "user-id-123" - - describe "with a name", -> - beforeEach -> - @req.body = name: @name = "new-name" - @controller.renameTag @req, @res - - it "should delete the tag in the backend", -> - @handler.renameTag - .calledWith(@user_id, @tag_id, @name) - .should.equal true - - it "should return 204 status code", -> - @res.status.calledWith(204).should.equal true - @res.end.called.should.equal true - - describe "without a name", -> - beforeEach -> - @controller.renameTag @req, @res - - it "should not call the backend", -> - @handler.renameTag.called.should.equal false - - it "should return 400 (bad request) status code", -> - @res.status.calledWith(400).should.equal true - @res.end.called.should.equal true - - describe "addProjectToTag", -> - beforeEach -> - @req.params.tag_id = @tag_id = "tag-id-123" - @req.params.project_id = @project_id = "project-id-123" - @req.session.user._id = @user_id = "user-id-123" - @controller.addProjectToTag @req, @res - - it "should add the tag to the project in the backend", -> - @handler.addProjectToTag - .calledWith(@user_id, @tag_id, @project_id) - .should.equal true - - it "should return 204 status code", -> - @res.status.calledWith(204).should.equal true - @res.end.called.should.equal true - - describe "removeProjectFromTag", -> - beforeEach -> - @req.params.tag_id = @tag_id = "tag-id-123" - @req.params.project_id = @project_id = "project-id-123" - @req.session.user._id = @user_id = "user-id-123" - @controller.removeProjectFromTag @req, @res - - it "should remove the tag from the project in the backend", -> - @handler.removeProjectFromTag - .calledWith(@user_id, @tag_id, @project_id) - .should.equal true - - it "should return 204 status code", -> - @res.status.calledWith(204).should.equal true - @res.end.called.should.equal true diff --git a/services/web/test/unit/coffee/Tags/TagsHandlerTests.coffee b/services/web/test/unit/coffee/Tags/TagsHandlerTests.coffee deleted file mode 100644 index 016b025169..0000000000 --- a/services/web/test/unit/coffee/Tags/TagsHandlerTests.coffee +++ /dev/null @@ -1,268 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('chai').assert -require('chai').should() -sinon = require('sinon') -modulePath = require('path').join __dirname, '../../../../app/js/Features/Tags/TagsHandler.js' -_ = require('underscore') - - -describe 'TagsHandler', -> - user_id = "user-id-123" - tag_id = "tag-id-123" - project_id = "project-id-123" - tagsUrl = "tags.sharelatex.testing" - tag = "tag_name" - - beforeEach -> - @request = - post: sinon.stub().callsArgWith(1) - del: sinon.stub().callsArgWith(1) - get: sinon.stub() - @callback = sinon.stub() - @handler = SandboxedModule.require modulePath, requires: - "settings-sharelatex": apis:{tags:{url:tagsUrl}} - "request":@request - 'logger-sharelatex': - log:-> - err:-> - - describe "removeProjectFromAllTags", -> - it 'should tell the tags api to remove the project_id from all the users tags', (done)-> - @handler.removeProjectFromAllTags user_id, project_id, => - @request.del.calledWith({url:"#{tagsUrl}/user/#{user_id}/project/#{project_id}", timeout:1000}).should.equal true - done() - - describe "_groupTagsByProject", -> - it 'should group the tags by project_id', (done)-> - rawTags = [ - {name:"class101", project_ids:["1234", "51db33e31a55afd212000007"]} - {name:"class201", project_ids:["1234", "51db33e31a55afd212000007"]} - {name:"research group", project_ids:["12", "51da65f2e2c39a2f09000100", "odjaskdas","dasdsa"]} - {name:"different", project_ids:["1234", "e2c39a2f09000100"]} - ] - - @handler._groupTagsByProject rawTags, (err, tags)-> - _.size(tags).should.equal 7 - done() - - describe "_requestTags", -> - it 'should return an err and empty array on error', (done)-> - @request.get.callsArgWith(1, {something:"wrong"}, {statusCode:200}, []) - @handler._requestTags user_id, (err, allTags)=> - allTags.length.should.equal 0 - assert.isDefined err - done() - - it 'should return an err and empty array on no body', (done)-> - @request.get.callsArgWith(1, {something:"wrong"}, {statusCode:200}, undefined) - @handler._requestTags user_id, (err, allTags)=> - allTags.length.should.equal 0 - assert.isDefined err - done() - - it 'should return an err and empty array on non 200 response', (done)-> - @request.get.callsArgWith(1, null, {statusCode:201}, []) - @handler._requestTags user_id, (err, allTags)=> - allTags.length.should.equal 0 - assert.isDefined err - done() - - it 'should return an err and empty array on no body and no response', (done)-> - @request.get.callsArgWith(1, {something:"wrong"}, undefined, undefined) - @handler._requestTags user_id, (err, allTags)=> - allTags.length.should.equal 0 - assert.isDefined err - done() - - describe "getAllTags", -> - it 'should get all tags', (done)-> - stubbedAllTags = [{name:"tag", project_ids:["123423","423423"]}] - @request.get.callsArgWith(1, null, {statusCode:200}, stubbedAllTags) - @handler.getAllTags user_id, (err, allTags)=> - stubbedAllTags.should.deep.equal allTags - getOpts = - url: "#{tagsUrl}/user/#{user_id}/tag" - json:true - timeout:1000 - @request.get.calledWith(getOpts).should.equal true - done() - - it 'should return empty arrays if there are no tags', -> - @request.get.callsArgWith(1, null, {statusCode:200}, null) - @handler.getAllTags user_id, (err, allTags, projectGroupedTags)=> - allTags.length.should.equal 0 - _.size(projectGroupedTags).should.equal 0 - - describe "createTag", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @handler.createTag user_id, @name = "tag_name", @callback - - it "should send a request to the tag backend", -> - @request.post - .calledWith({ - url: "#{tagsUrl}/user/#{user_id}/tag" - json: - name: @name - timeout: 1000 - }) - .should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "deleteTag", -> - describe "successfully", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @handler.deleteTag user_id, tag_id, @callback - - it "should send a request to the tag backend", -> - @request.del - .calledWith({ - url: "#{tagsUrl}/user/#{user_id}/tag/#{tag_id}" - timeout: 1000 - }) - .should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "with error", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 500}, "") - @handler.deleteTag user_id, tag_id, @callback - - it "should call the callback with an Error", -> - @callback.calledWith(new Error()).should.equal true - - describe "renameTag", -> - describe "successfully", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @handler.renameTag user_id, tag_id, @name = "new-name", @callback - - it "should send a request to the tag backend", -> - @request.post - .calledWith({ - url: "#{tagsUrl}/user/#{user_id}/tag/#{tag_id}/rename" - json: - name: @name - timeout: 1000 - }) - .should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "with error", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, "") - @handler.renameTag user_id, tag_id, "name", @callback - - it "should call the callback with an Error", -> - @callback.calledWith(new Error()).should.equal true - - describe "removeProjectFromTag", -> - describe "successfully", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @handler.removeProjectFromTag user_id, tag_id, project_id, @callback - - it "should send a request to the tag backend", -> - @request.del - .calledWith({ - url: "#{tagsUrl}/user/#{user_id}/tag/#{tag_id}/project/#{project_id}" - timeout: 1000 - }) - .should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "with error", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 500}, "") - @handler.removeProjectFromTag user_id, tag_id, project_id, @callback - - it "should call the callback with an Error", -> - @callback.calledWith(new Error()).should.equal true - - describe "addProjectToTag", -> - describe "successfully", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @handler.addProjectToTag user_id, tag_id, project_id, @callback - - it "should send a request to the tag backend", -> - @request.post - .calledWith({ - url: "#{tagsUrl}/user/#{user_id}/tag/#{tag_id}/project/#{project_id}" - timeout: 1000 - }) - .should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "with error", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, "") - @handler.addProjectToTag user_id, tag_id, project_id, @callback - - it "should call the callback with an Error", -> - @callback.calledWith(new Error()).should.equal true - - describe "addProjectToTagName", -> - describe "successfully", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @handler.addProjectToTagName user_id, tag, project_id, @callback - - it "should send a request to the tag backend", -> - @request.post - .calledWith({ - json: - name: tag - url: "#{tagsUrl}/user/#{user_id}/tag/project/#{project_id}" - timeout: 1000 - }) - .should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "with error", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, "") - @handler.addProjectToTagName user_id, tag_id, project_id, @callback - - it "should call the callback with an Error", -> - @callback.calledWith(new Error()).should.equal true - - describe "updateTagUserIds", -> - describe "successfully", -> - beforeEach -> - @request.put = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @handler.updateTagUserIds "old-user-id", "new-user-id", @callback - - it "should send a request to the tag backend", -> - @request.put - .calledWith({ - json: - user_id: "new-user-id" - url: "#{tagsUrl}/user/old-user-id/tag" - timeout: 1000 - }) - .should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "with error", -> - beforeEach -> - @request.put = sinon.stub().callsArgWith(1, null, {statusCode: 500}, "") - @handler.updateTagUserIds "old-user-id", "new-user-id", @callback - - it "should call the callback with an Error", -> - @callback.calledWith(new Error()).should.equal true diff --git a/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee b/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee deleted file mode 100644 index 78344ca083..0000000000 --- a/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee +++ /dev/null @@ -1,67 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -chai = require('chai') -sinon = require('sinon') - -chai.should() -expect = chai.expect - -modulePath = '../../../../app/js/Features/Templates/TemplatesController' - -describe "TemplatesController", -> - - beforeEach -> - @user_id = "user-id" - @TemplatesController = SandboxedModule.require modulePath, requires: - "../../../js/Features/Authentication/AuthenticationController": @AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(@user_id) - } - "./TemplatesManager": @TemplatesManager = { - createProjectFromV1Template: sinon.stub() - } - "logger-sharelatex": - log:-> - err:-> - @next = sinon.stub() - @req = - body: - brandVariationId: "brand-variation-id" - compiler: "compiler" - mainFile: "main-file" - templateId: "template-id" - templateName: "template-name" - templateVersionId: "template-version-id" - session: - templateData: "template-data" - user: _id: @user_id - @res = - redirect: sinon.stub() - - describe "createProjectFromV1Template", -> - - describe "on success", -> - beforeEach -> - @project = - _id: "project-id" - @TemplatesManager.createProjectFromV1Template.yields null, @project - @TemplatesController.createProjectFromV1Template @req, @res, @next - - it "should call TemplatesManager", -> - @TemplatesManager.createProjectFromV1Template.should.have.been.calledWithMatch "brand-variation-id", "compiler", "main-file", "template-id", "template-name", "template-version-id", "user-id" - - it "should redirect to project", -> - @res.redirect.should.have.been.calledWith "/project/project-id" - - it "should delete session", -> - expect(@req.session.templateData).to.be.undefined - - describe "on error", -> - beforeEach -> - @TemplatesManager.createProjectFromV1Template.yields "error" - @TemplatesController.createProjectFromV1Template @req, @res, @next - - it "should call next with error", -> - @next.should.have.been.calledWith "error" - - it "should not redirect", -> - @res.redirect.called.should.equal false diff --git a/services/web/test/unit/coffee/Templates/TemplatesManagerTests.coffee b/services/web/test/unit/coffee/Templates/TemplatesManagerTests.coffee deleted file mode 100644 index dcebb2454e..0000000000 --- a/services/web/test/unit/coffee/Templates/TemplatesManagerTests.coffee +++ /dev/null @@ -1,117 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -chai = require('chai') -sinon = require('sinon') - -should = require('chai').should() - -modulePath = '../../../../app/js/Features/Templates/TemplatesManager' - -describe 'TemplatesManager', -> - - beforeEach -> - @project_id = "project-id" - @brandVariationId = "brand-variation-id" - @compiler = "pdflatex" - @imageName = "TL2017" - @mainFile = "main.tex" - @templateId = "template-id" - @templateName = "template name" - @templateVersionId = "template-version-id" - @user_id = "user-id" - @dumpPath = "#{@dumpFolder}/#{@uuid}" - @callback = sinon.stub() - @request = sinon.stub().returns { - pipe:-> - on:-> - response: statusCode: 200 - } - @fs = { - unlink : sinon.stub() - createWriteStream : sinon.stub().returns(on: sinon.stub().yields()) - } - @ProjectUploadManager = {createProjectFromZipArchiveWithName : sinon.stub().callsArgWith(3, null, {_id:@project_id})} - @dumpFolder = "dump/path" - @ProjectOptionsHandler = { - setCompiler:sinon.stub().callsArgWith(2) - setImageName:sinon.stub().callsArgWith(2) - setBrandVariationId:sinon.stub().callsArgWith(2) - } - @uuid = "1234" - @ProjectRootDocManager = { - setRootDocFromName: sinon.stub().callsArgWith(2) - } - @ProjectDetailsHandler = - getProjectDescription:sinon.stub() - fixProjectName: sinon.stub().returns(@templateName) - @Project = - update: sinon.stub().callsArgWith(3, null) - @FileWriter = - ensureDumpFolderExists: sinon.stub().callsArg(0) - @TemplatesManager = SandboxedModule.require modulePath, requires: - '../../../js/Features/Uploads/ProjectUploadManager':@ProjectUploadManager - '../../../js/Features/Project/ProjectOptionsHandler':@ProjectOptionsHandler - '../../../js/Features/Project/ProjectRootDocManager':@ProjectRootDocManager - '../../../js/Features/Project/ProjectDetailsHandler':@ProjectDetailsHandler - '../../../js/Features/Authentication/AuthenticationController': @AuthenticationController = {getLoggedInUserId: sinon.stub()} - '../../infrastructure/FileWriter': @FileWriter - './TemplatesPublisher':@TemplatesPublisher - "logger-sharelatex": - log:-> - err:-> - "settings-sharelatex": - path: - dumpFolder:@dumpFolder - siteUrl: @siteUrl = "http://localhost:3000" - apis: - v1: - url: @v1Url="http://overleaf.com" - user: "sharelatex" - pass: "password" - overleaf: - host: @v1Url - "uuid":v4:=>@uuid - "request": @request - "fs":@fs - "../../../js/models/Project": {Project: @Project} - @zipUrl = "%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex" - - describe 'createProjectFromV1Template', -> - - describe "when all options passed", -> - beforeEach -> - @TemplatesManager.createProjectFromV1Template @brandVariationId, @compiler, @mainFile, @templateId, @templateName, @templateVersionId, @user_id, @imageName, @callback - - it "should fetch zip from v1 based on template id", -> - @request.should.have.been.calledWith "#{@v1Url}/api/v1/sharelatex/templates/#{@templateVersionId}" - - it "should save temporary file", -> - @fs.createWriteStream.should.have.been.calledWith @dumpPath - - it "should create project", -> - @ProjectUploadManager.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch @user_id, @templateName, @dumpPath - - it "should unlink file", -> - @fs.unlink.should.have.been.calledWith @dumpPath - - it "should set project options when passed", -> - @ProjectOptionsHandler.setCompiler.should.have.been.calledWithMatch @project_id, @compiler - @ProjectOptionsHandler.setImageName.should.have.been.calledWithMatch @project_id, @imageName - @ProjectRootDocManager.setRootDocFromName.should.have.been.calledWithMatch @project_id, @mainFile - @ProjectOptionsHandler.setBrandVariationId.should.have.been.calledWithMatch @project_id, @brandVariationId - - it "should update project", -> - @Project.update.should.have.been.calledWithMatch { _id: @project_id }, { fromV1TemplateId: @templateId, fromV1TemplateVersionId: @templateVersionId } - - it "should ensure that the dump folder exists", -> - sinon.assert.called(@FileWriter.ensureDumpFolderExists) - - describe "when some options not set", -> - beforeEach -> - @TemplatesManager.createProjectFromV1Template null, null, null, @templateId, @templateName, @templateVersionId, @user_id, null, @callback - - it "should not set missing project options", -> - @ProjectOptionsHandler.setCompiler.called.should.equal false - @ProjectRootDocManager.setRootDocFromName.called.should.equal false - @ProjectOptionsHandler.setBrandVariationId.called.should.equal false - @ProjectOptionsHandler.setImageName.should.have.been.calledWithMatch @project_id, "wl_texlive:2018.1" diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee deleted file mode 100644 index ba5c507cfb..0000000000 --- a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee +++ /dev/null @@ -1,127 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/ThirdPartyDataStore/TpdsController.js' - - - -describe 'TpdsController', -> - beforeEach -> - @TpdsUpdateHandler = {} - @TpdsController = SandboxedModule.require modulePath, requires: - './TpdsUpdateHandler':@TpdsUpdateHandler - './UpdateMerger': @UpdateMerger = {} - 'logger-sharelatex': - log:-> - err:-> - "metrics-sharelatex": inc:-> - - @user_id = "dsad29jlkjas" - - describe 'getting an update', -> - - it 'should process the update with the update reciver', (done)-> - path = "/projectName/here.txt" - req = - pause:-> - params:{0:path, "user_id":@user_id} - session: - destroy:-> - headers: - "x-sl-update-source": @source = "dropbox" - @TpdsUpdateHandler.newUpdate = sinon.stub().callsArg(5) - res = sendStatus: => - @TpdsUpdateHandler.newUpdate.calledWith(@user_id, "projectName","/here.txt", req, @source).should.equal true - done() - @TpdsController.mergeUpdate req, res - - describe 'getting a delete update', -> - it 'should process the delete with the update reciver', (done)-> - path = "/projectName/here.txt" - req = - params:{0:path, "user_id":@user_id} - session: - destroy:-> - headers: - "x-sl-update-source": @source = "dropbox" - @TpdsUpdateHandler.deleteUpdate = sinon.stub().callsArg(4) - res = sendStatus: => - @TpdsUpdateHandler.deleteUpdate.calledWith(@user_id, "projectName", "/here.txt", @source).should.equal true - done() - @TpdsController.deleteUpdate req, res - - describe 'parseParams', -> - - it 'should take the project name off the start and replace with slash', -> - path = "noSlashHere" - req = params:{0:path, user_id:@user_id} - result = @TpdsController.parseParams(req) - result.user_id.should.equal @user_id - result.filePath.should.equal "/" - result.projectName.should.equal path - - - it 'should take the project name off the start and return it with no slashes in', -> - path = "/project/file.tex" - req = params:{0:path, user_id:@user_id} - result = @TpdsController.parseParams(req) - result.user_id.should.equal @user_id - result.filePath.should.equal "/file.tex" - result.projectName.should.equal "project" - - it 'should take the project name of and return a slash for the file path', -> - path = "/project_name" - req = params:{0:path, user_id:@user_id} - result = @TpdsController.parseParams(req) - result.projectName.should.equal "project_name" - result.filePath.should.equal "/" - - describe 'updateProjectContents', -> - beforeEach -> - @UpdateMerger.mergeUpdate = sinon.stub().callsArg(5) - @req = - params: - 0: @path = "chapters/main.tex" - project_id: @project_id = "project-id-123" - session: - destroy: sinon.stub() - headers: - "x-sl-update-source": @source = "github" - @res = - sendStatus: sinon.stub() - - @TpdsController.updateProjectContents @req, @res - - it "should merge the update", -> - @UpdateMerger.mergeUpdate - .calledWith(null, @project_id, "/" + @path, @req, @source) - .should.equal true - - it "should return a success", -> - @res.sendStatus.calledWith(200).should.equal true - - - describe 'deleteProjectContents', -> - beforeEach -> - @UpdateMerger.deleteUpdate = sinon.stub().callsArg(4) - @req = - params: - 0: @path = "chapters/main.tex" - project_id: @project_id = "project-id-123" - session: - destroy: sinon.stub() - headers: - "x-sl-update-source": @source = "github" - @res = - sendStatus: sinon.stub() - - @TpdsController.deleteProjectContents @req, @res - - it "should delete the file", -> - @UpdateMerger.deleteUpdate - .calledWith(null, @project_id, "/" + @path, @source) - .should.equal true - - it "should return a success", -> - @res.sendStatus.calledWith(200).should.equal true - diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee deleted file mode 100644 index 3984d6341f..0000000000 --- a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee +++ /dev/null @@ -1,124 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -expect = require('chai').expect -modulePath = require('path').join __dirname, '../../../../app/js/Features/ThirdPartyDataStore/TpdsUpdateHandler.js' - -describe 'TpdsUpdateHandler', -> - beforeEach -> - @requestQueuer = {} - @updateMerger = - deleteUpdate: (user_id, project_id, path, source, cb)->cb() - mergeUpdate:(user_id, project_id, path, update, source, cb)->cb() - @editorController = {} - @project_id = "dsjajilknaksdn" - @project = {_id:@project_id, name:"projectNameHere"} - @projectLocator = findUsersProjectByName:sinon.stub().callsArgWith(2, null, @project) - @projectCreationHandler = - createBlankProject : sinon.stub().callsArgWith(2, null, @project) - @projectDeleter = {markAsDeletedByExternalSource:sinon.stub().callsArgWith(1)} - @rootDocManager = setRootDocAutomatically:sinon.stub() - @FileTypeManager = - shouldIgnore: sinon.stub().callsArgWith(1, null, false) - @CooldownManager = - isProjectOnCooldown: sinon.stub().callsArgWith(1, null, false) - @handler = SandboxedModule.require modulePath, requires: - './UpdateMerger': @updateMerger - './Editor/EditorController': @editorController - '../Project/ProjectLocator': @projectLocator - '../Project/ProjectCreationHandler':@projectCreationHandler - '../Project/ProjectDeleter': @projectDeleter - "../Project/ProjectRootDocManager" : @rootDocManager - '../Uploads/FileTypeManager': @FileTypeManager - '../Cooldown/CooldownManager': @CooldownManager - 'logger-sharelatex': log:-> - @user_id = "dsad29jlkjas" - @source = "dropbox" - - describe 'getting an update', -> - it 'should send the update to the update merger', (done)-> - path = "/path/here" - update = {} - @updateMerger.mergeUpdate = sinon.stub() - @updateMerger.mergeUpdate.withArgs(@user_id, @project_id, path, update, @source).callsArg(5) - @handler.newUpdate @user_id, @project.name, path, update, @source, => - @projectCreationHandler.createBlankProject.called.should.equal false - done() - - it 'should create a new project if one does not already exit', (done)-> - @projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) - path = "/" - @handler.newUpdate @user_id, @project.name, path, {}, @source, => - @projectCreationHandler.createBlankProject.calledWith(@user_id, @project.name).should.equal true - done() - - it 'should set the root doc automatically if a new project is created', (done)-> - @projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) - @handler._rootDocTimeoutLength = 0 - path = "/" - @handler.newUpdate @user_id, @project.name, path, {}, @source, => - setTimeout (=> - @rootDocManager.setRootDocAutomatically.calledWith(@project._id).should.equal true - done() - ), 1 - - it 'should not update files that should be ignored', (done) -> - @FileTypeManager.shouldIgnore = sinon.stub().callsArgWith(1, null, true) - @projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) - path = "/.gitignore" - @updateMerger.mergeUpdate = sinon.stub() - @handler.newUpdate @user_id, @project.name, path, {}, @source, => - @updateMerger.mergeUpdate.called.should.equal false - done() - - it 'should check if the project is on cooldown', (done) -> - @CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, false) - @projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) - path = "/path/here" - update = {} - @updateMerger.mergeUpdate = sinon.stub() - @updateMerger.mergeUpdate.withArgs(@user_id, @project_id, path, update, @source).callsArg(5) - @handler.newUpdate @user_id, @project.name, path, update, @source, (err) => - expect(err).to.be.oneOf [null, undefined] - @CooldownManager.isProjectOnCooldown.callCount.should.equal 1 - @CooldownManager.isProjectOnCooldown.calledWith(@project_id).should.equal true - @FileTypeManager.shouldIgnore.callCount.should.equal 1 - @updateMerger.mergeUpdate.callCount.should.equal 1 - done() - - it 'should return error and not proceed with update if project is on cooldown', (done) -> - @CooldownManager.isProjectOnCooldown = sinon.stub().callsArgWith(1, null, true) - @projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) - @FileTypeManager.shouldIgnore = sinon.stub().callsArgWith(1, null, false) - path = "/path/here" - update = {} - @updateMerger.mergeUpdate = sinon.stub() - @updateMerger.mergeUpdate.withArgs(@user_id, @project_id, path, update, @source).callsArg(5) - @handler.newUpdate @user_id, @project.name, path, update, @source, (err) => - expect(err).to.not.be.oneOf [null, undefined] - expect(err).to.be.instanceof Error - @CooldownManager.isProjectOnCooldown.callCount.should.equal 1 - @CooldownManager.isProjectOnCooldown.calledWith(@project_id).should.equal true - @FileTypeManager.shouldIgnore.callCount.should.equal 0 - @updateMerger.mergeUpdate.callCount.should.equal 0 - done() - - describe 'getting a delete :', -> - it 'should call deleteEntity in the collaberation manager', (done)-> - path = "/delete/this" - update = {} - @updateMerger.deleteUpdate = sinon.stub().callsArg(4) - - @handler.deleteUpdate @user_id, @project.name, path, @source, => - @projectDeleter.markAsDeletedByExternalSource.calledWith(@project._id).should.equal false - @updateMerger.deleteUpdate - .calledWith(@user_id, @project_id, path, @source) - .should.equal true - done() - - it 'should mark the project as deleted by external source if path is a single slash', (done)-> - path = "/" - @handler.deleteUpdate @user_id, @project.name, path, @source, => - @projectDeleter.markAsDeletedByExternalSource.calledWith(@project._id).should.equal true - done() - diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee deleted file mode 100644 index b0378ea8fc..0000000000 --- a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee +++ /dev/null @@ -1,153 +0,0 @@ -SandboxedModule = require('sandboxed-module') -assert = require('assert') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/ThirdPartyDataStore/TpdsUpdateSender.js' -sinon = require('sinon') -ath = require('path') -project_id = "project_id_here" -user_id = "user_id_here" -read_only_ref_1 = "read_only_ref_1_id_here" -collaberator_ref_1 = "collaberator_ref_1_here" -project_name = "project_name_here" - -thirdPartyDataStoreApiUrl = "http://third-party-json-store.herokuapp.com" -httpUsername = "user" -httpPass = "pass" -siteUrl = "http://www.localhost:3000" -httpAuthSiteUrl = "http://#{httpUsername}:#{httpPass}@www.localhost:3000" -filestoreUrl = "filestore.sharelatex.com" - -describe 'TpdsUpdateSender', -> - beforeEach -> - @requestQueuer = (queue, meth, opts, callback)-> - project = {owner_ref:user_id} - member_ids = [collaberator_ref_1, read_only_ref_1, user_id] - @CollaboratorsHandler = - getInvitedMemberIds: sinon.stub().yields(null, member_ids) - @ProjectGetter = getProject: sinon.stub().callsArgWith(2, null, project) - @docstoreUrl = "docstore.sharelatex.env" - @request = sinon.stub().returns(pipe:->) - @settings = - siteUrl:siteUrl - httpAuthSiteUrl:httpAuthSiteUrl, - apis: - thirdPartyDataStore: {url: thirdPartyDataStoreApiUrl} - filestore: - url: filestoreUrl - docstore: - pubUrl: @docstoreUrl - @updateSender = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "logger-sharelatex":{log:->} - '../Project/ProjectGetter': @ProjectGetter - 'request':@request - '../Collaborators/CollaboratorsHandler': @CollaboratorsHandler - "metrics-sharelatex": - inc:-> - - describe "_enqueue", -> - - it "should not call request if there is no tpdsworker url", (done)-> - @updateSender._enqueue null, null, null, (err)=> - @request.called.should.equal false - done() - - it "should post the message to the tpdsworker", (done)-> - @settings.apis.tpdsworker = url:"www.tpdsworker.env" - group = "myproject" - method = "somemethod" - job = "do something" - @request.callsArgWith(1) - @updateSender._enqueue group, method, job, (err)=> - args = @request.args[0][0] - args.json.group.should.equal group - args.json.job.should.equal job - args.json.method.should.equal method - args.uri.should.equal "www.tpdsworker.env/enqueue/web_to_tpds_http_requests" - done() - - - - describe 'sending updates', -> - - it 'queues a post the file with user and file id', (done)-> - file_id = '4545345' - path = '/some/path/here.jpg' - @updateSender._enqueue = (uid, method, job, callback)-> - uid.should.equal project_id - job.method.should.equal "post" - job.streamOrigin.should.equal "#{filestoreUrl}/project/#{project_id}/file/#{file_id}" - expectedUrl = "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity/#{encodeURIComponent(project_name)}#{encodeURIComponent(path)}" - job.uri.should.equal expectedUrl - job.headers.sl_all_user_ids.should.eql(JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id])) - done() - @updateSender.addFile {project_id:project_id, file_id:file_id, path:path, project_name:project_name}, -> - - it 'post doc with stream origin of docstore', (done)-> - doc_id = "4545345" - path = "/some/path/here.tex" - lines = ["line1", "line2", "line3"] - - @updateSender._enqueue = (uid, method, job, callback)=> - uid.should.equal project_id - job.method.should.equal "post" - expectedUrl = "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity/#{encodeURIComponent(project_name)}#{encodeURIComponent(path)}" - job.uri.should.equal expectedUrl - job.streamOrigin.should.equal "#{@docstoreUrl}/project/#{project_id}/doc/#{doc_id}/raw" - job.headers.sl_all_user_ids.should.eql(JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id])) - done() - @updateSender.addDoc {project_id:project_id, doc_id:doc_id, path:path, docLines:lines,project_name:project_name} - - it 'deleting entity', (done)-> - path = "/path/here/t.tex" - @updateSender._enqueue = (uid, method, job, callback)-> - uid.should.equal project_id - job.method.should.equal "DELETE" - expectedUrl = "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity/#{encodeURIComponent(project_name)}#{encodeURIComponent(path)}" - job.headers.sl_all_user_ids.should.eql(JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id])) - job.uri.should.equal expectedUrl - done() - @updateSender.deleteEntity {project_id:project_id, path:path, project_name:project_name} - - it 'moving entity', (done)-> - startPath = "staring/here/file.tex" - endPath = "ending/here/file.tex" - @updateSender._enqueue = (uid, method, job, callback)-> - uid.should.equal project_id - job.method.should.equal "put" - job.uri.should.equal "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity" - job.json.startPath.should.equal "/#{project_name}/#{startPath}" - job.json.endPath.should.equal "/#{project_name}/#{endPath}" - job.headers.sl_all_user_ids.should.eql(JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id])) - done() - @updateSender.moveEntity {project_id:project_id, startPath:startPath, endPath:endPath, project_name:project_name} - - it 'should be able to rename a project using the move entity func', (done)-> - oldProjectName = "/oldProjectName/" - newProjectName = "/newProjectName/" - @updateSender._enqueue = (uid, method, job, callback)-> - uid.should.equal project_id - job.method.should.equal "put" - job.uri.should.equal "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity" - job.json.startPath.should.equal oldProjectName - job.json.endPath.should.equal newProjectName - job.headers.sl_all_user_ids.should.eql(JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id])) - done() - @updateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newProjectName} - - it "pollDropboxForUser", (done) -> - @updateSender._enqueue = sinon.stub().callsArg(3) - @updateSender.pollDropboxForUser user_id, (error) => - @updateSender._enqueue - .calledWith( - "poll-dropbox:#{user_id}", - "standardHttpRequest", - { - method: "POST" - uri: "#{thirdPartyDataStoreApiUrl}/user/poll" - json: - user_ids: [user_id] - } - ) - .should.equal true - done() diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee deleted file mode 100644 index e65c8c1c50..0000000000 --- a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee +++ /dev/null @@ -1,156 +0,0 @@ -Stream = require('stream') -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/ThirdPartyDataStore/UpdateMerger.js' -BufferedStream = require('bufferedstream') - -describe 'UpdateMerger :', -> - beforeEach -> - @updateMerger = SandboxedModule.require modulePath, requires: - 'fs': @fs = - unlink:sinon.stub().callsArgWith(1) - 'logger-sharelatex': - log: -> - err: -> - '../Editor/EditorController': @EditorController = {} - '../Uploads/FileTypeManager':@FileTypeManager = {} - '../../infrastructure/FileWriter': @FileWriter = {} - '../Project/ProjectEntityHandler': @ProjectEntityHandler = {} - 'settings-sharelatex':{path:{dumpPath:"dump_here"}} - @project_id = "project_id_here" - @user_id = "mock-user-id" - - @docPath = @newDocPath = "/folder/doc.tex" - @filePath = @newFilePath = "/folder/file.png" - - @existingDocPath = '/folder/other.tex' - @existingFilePath = '/folder/fig1.pdf' - - @linkedFileData = {provider: 'url'} - - @existingDocs = [ - {path: '/main.tex'} - {path: '/folder/other.tex'} - ] - @existingFiles = [ - {path: '/figure.pdf'} - {path: '/folder/fig1.pdf'} - ] - @ProjectEntityHandler.getAllEntities = sinon.stub().callsArgWith(1, null, @existingDocs, @existingFiles) - - @fsPath = "/tmp/file/path" - @source = "dropbox" - @updateRequest = new BufferedStream() - @FileWriter.writeStreamToDisk = sinon.stub().yields(null, @fsPath) - @callback = sinon.stub() - - describe 'mergeUpdate', -> - - describe "doc updates for a new doc", -> - - beforeEach -> - @FileTypeManager.getType = sinon.stub().yields(null, false) - @updateMerger.p.processDoc = sinon.stub().yields() - @updateMerger.mergeUpdate @user_id, @project_id, @docPath, @updateRequest, @source, @callback - - it 'should look at the file contents', -> - @FileTypeManager.getType.called.should.equal true - - it 'should process update as doc', -> - @updateMerger.p.processDoc - .calledWith(@project_id, @user_id, @fsPath, @docPath, @source) - .should.equal true - - it 'removes the temp file from disk', -> - @fs.unlink.calledWith(@fsPath).should.equal true - - describe "file updates for a new file ", -> - beforeEach -> - @FileTypeManager.getType = sinon.stub().yields(null, true) - @updateMerger.p.processFile = sinon.stub().yields() - @updateMerger.mergeUpdate @user_id, @project_id, @filePath, @updateRequest, @source, @callback - - it 'should look at the file contents', -> - @FileTypeManager.getType.called.should.equal true - - it 'should process update as file', -> - @updateMerger.p.processFile - .calledWith(@project_id, @fsPath, @filePath, @source, @user_id) - .should.equal true - - it 'removes the temp file from disk', -> - @fs.unlink.calledWith(@fsPath).should.equal true - - describe "doc updates for an existing doc", -> - beforeEach -> - @FileTypeManager.getType = sinon.stub() - @updateMerger.p.processDoc = sinon.stub().yields() - @updateMerger.mergeUpdate @user_id, @project_id, @existingDocPath, @updateRequest, @source, @callback - - it 'should not look at the file contents', -> - @FileTypeManager.getType.called.should.equal false - - it 'should process update as doc', -> - @updateMerger.p.processDoc - .calledWith(@project_id, @user_id, @fsPath, @existingDocPath, @source) - .should.equal true - - it 'removes the temp file from disk', -> - @fs.unlink.calledWith(@fsPath).should.equal true - - describe "file updates for an existing file", -> - beforeEach -> - @FileTypeManager.getType = sinon.stub() - @updateMerger.p.processFile = sinon.stub().yields() - @updateMerger.mergeUpdate @user_id, @project_id, @existingFilePath, @updateRequest, @source, @callback - - it 'should not look at the file contents', -> - @FileTypeManager.getType.called.should.equal false - - it 'should process update as file', -> - @updateMerger.p.processFile - .calledWith(@project_id, @fsPath, @existingFilePath, @source, @user_id) - .should.equal true - - it 'removes the temp file from disk', -> - @fs.unlink.calledWith(@fsPath).should.equal true - - describe 'deleteUpdate', -> - beforeEach -> - @EditorController.deleteEntityWithPath = sinon.stub().yields() - @updateMerger.deleteUpdate @user_id, @project_id, @docPath, @source, @callback - - it 'should delete the entity in the editor controller', -> - @EditorController.deleteEntityWithPath - .calledWith(@project_id, @docPath, @source, @user_id) - .should.equal true - - describe 'private methods', -> - describe 'processDoc', -> - beforeEach -> - @docLines = "\\documentclass{article}\n\\usepackage[utf8]{inputenc}\n\n\\title{42}\n\\author{Jane Doe}\n\\date{June 2011}" - @updateMerger.p.readFileIntoTextArray = sinon.stub().yields(null, @docLines) - @EditorController.upsertDocWithPath = sinon.stub().yields() - - @updateMerger.p.processDoc @project_id, @user_id, @fsPath, @docPath, @source, @callback - - it 'reads the temp file from disk', -> - @updateMerger.p.readFileIntoTextArray - .calledWith(@fsPath) - .should.equal true - - it 'should upsert the doc in the editor controller', -> - @EditorController.upsertDocWithPath - .calledWith(@project_id, @docPath, @docLines, @source, @user_id) - .should.equal true - - describe 'processFile', -> - beforeEach -> - @EditorController.upsertFileWithPath = sinon.stub().yields() - @updateMerger.p.processFile @project_id, @fsPath, @filePath, @source, @user_id, @callback - - it 'should upsert the file in the editor controller', -> - @EditorController.upsertFileWithPath - .calledWith(@project_id, @filePath, @fsPath, null, @source, @user_id) - .should.equal true diff --git a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee deleted file mode 100644 index 0ac9368063..0000000000 --- a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee +++ /dev/null @@ -1,1221 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/TokenAccess/TokenAccessController" -expect = require("chai").expect -ObjectId = require("mongojs").ObjectId -MockRequest = require('../helpers/MockRequest') -MockResponse = require('../helpers/MockResponse') -Errors = require "../../../../app/js/Features/Errors/Errors.js" - -describe "TokenAccessController", -> - - beforeEach -> - @readOnlyToken = 'somereadonlytoken' - @readAndWriteToken = '42somereadandwritetoken' - @projectId = ObjectId() - @ownerId = 'owner' - @project = - _id: @projectId - publicAccesLevel: 'tokenBased' - tokens: - readOnly: @readOnlyToken - readAndWrite: @readAndWriteToken - owner_ref: @ownerId - @userId = ObjectId() - @TokenAccessController = SandboxedModule.require modulePath, requires: - '../Project/ProjectController': @ProjectController = {} - '../Authentication/AuthenticationController': @AuthenticationController = {} - './TokenAccessHandler': @TokenAccessHandler = { - getV1DocPublishedInfo: sinon.stub().yields(null, { - allow: true - }) - getV1DocInfo: sinon.stub().yields(null, { - exists: true - exported: false - }) - } - '../../infrastructure/Features': @Features = { - hasFeature: sinon.stub().returns(false) - } - 'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()} - 'settings-sharelatex': { - overleaf: - host: 'http://overleaf.test:5000' - } - '../V1/V1Api': @V1Api = { - request: sinon.stub().callsArgWith(1, null, {}, { allow: true }) - } - - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId.toString()) - - - describe 'readAndWriteToken', -> - beforeEach -> - - describe 'when all goes well', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @AuthenticationController.setRedirectInSession = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith(@readAndWriteToken)) - .to.equal true - done() - - it 'should add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 1 - expect(@TokenAccessHandler.addReadAndWriteUserToProject.calledWith( - @userId.toString(), @projectId - )) - .to.equal true - done() - - it 'should pass control to loadEditor', (done) -> - expect(@req.params.Project_id).to.equal @projectId.toString() - expect(@ProjectController.loadEditor.callCount).to.equal 1 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal true - done() - - describe 'when the user is already the owner', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @project.owner_ref = @userId - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith(@readAndWriteToken)) - .to.equal true - done() - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 0 - done() - - it 'should pass control to loadEditor', (done) -> - expect(@req.params.Project_id).to.equal @projectId.toString() - expect(@ProjectController.loadEditor.callCount).to.equal 1 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal true - done() - - - describe 'when there is no user', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = - sinon.stub().returns(null) - - describe 'when anonymous read-write access is enabled', -> - beforeEach -> - @TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessHandler.grantSessionTokenAccess = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 0 - done() - - it 'should give the user session token access', (done) -> - expect(@TokenAccessHandler.grantSessionTokenAccess.callCount) - .to.equal 1 - expect(@TokenAccessHandler.grantSessionTokenAccess.calledWith( - @req, @projectId, @readAndWriteToken - )) - .to.equal true - done() - - it 'should pass control to loadEditor', (done) -> - expect(@req.params.Project_id).to.equal @projectId.toString() - expect(@ProjectController.loadEditor.callCount).to.equal 1 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal true - done() - - describe 'when anonymous read-write access is not enabled', -> - beforeEach -> - @TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false - @req = new MockRequest() - @res = new MockResponse() - @res.redirect = sinon.stub() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessHandler.grantSessionTokenAccess = sinon.stub() - @AuthenticationController.setRedirectInSession = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 0 - done() - - it 'should give the user session token access', (done) -> - expect(@TokenAccessHandler.grantSessionTokenAccess.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should set redirect in session', (done) -> - expect(@AuthenticationController.setRedirectInSession.callCount).to.equal 1 - expect(@AuthenticationController.setRedirectInSession.calledWith(@req)).to.equal true - done() - - it 'should redirect to restricted page', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith('/restricted')).to.equal true - done() - - describe 'when findProject produces an error', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, new Error('woops')) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith(@readAndWriteToken)) - .to.equal true - done() - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should call next with an error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - describe 'when findProject does not find a project', -> - beforeEach -> - - describe 'when user is present', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = - sinon.stub().returns(@userId.toString()) - - describe 'when project does not exist', -> - beforeEach -> - @req = new MockRequest() - @req.url = '/123abc' - @res = new MockResponse() - @res.redirect = sinon.stub() - @res.render = sinon.stub() - @next = sinon.stub() - @req.params['read_and_write_token'] = '123abc' - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null, false) - - describe 'when project was not exported from v1', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should redirect to v1', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith( - 302, - '/sign_in_to_v1?return_to=/123abc' - )).to.equal true - done() - - describe 'when project was not exported from v1 but forcing import to v2', -> - beforeEach -> - @Features.hasFeature.returns(true) - - describe 'with project name', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: true - name: 'A title' - has_assignment: false - brand_info: null - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should render v2-import page with name', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: '123abc' - name: 'A title' - hasOwner: true - hasAssignment: false - brandInfo: null - } - )).to.equal true - done() - - describe 'with project owner', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: true - name: 'A title' - has_assignment: false - brand_info: null - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: '123abc' - hasOwner: true - name: 'A title' - hasAssignment: false - brandInfo: null - } - )).to.equal true - done() - - describe 'without project owner', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: false - name: 'A title' - has_assignment: false - brand_info: null - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: '123abc', - hasOwner: false - name: 'A title', - hasAssignment: false, - brandInfo: null - } - )).to.equal true - done() - - describe 'with assignment', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: false - name: 'A title' - has_assignment: true - brand_info: null - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: '123abc', - hasOwner: false - name: 'A title', - hasAssignment: true, - brandInfo: null - } - )).to.equal true - done() - - describe 'with brand info', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: false - name: 'A title' - has_assignment: false - brand_info: 'wellcome' - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: '123abc', - hasOwner: false - name: 'A title', - hasAssignment: false, - brandInfo: 'wellcome' - } - )).to.equal true - done() - - describe 'with anonymous user', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(null) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should render anonymous import status page', (done) -> - expect(@res.render.callCount).to.equal 1 - expect(@res.render.calledWith( - 'project/v2-import', - { loginRedirect: '/123abc' } - )).to.equal true - done() - - describe 'when project was exported from v1', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: true - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 1 - done() - - describe 'when project does not exist on v1', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: false - exported: false - }) - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.calledWith(new Errors.NotFoundError())).to.equal true - done() - - describe 'when token access is off, but user has higher access anyway', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @res.redirect = sinon.stub() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null, true) - @TokenAccessHandler.findProjectWithHigherAccess = - sinon.stub() - .callsArgWith(2, null, @project) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken - .calledWith(@readAndWriteToken) - ).to.equal true - done() - - it 'should check if user has higher access to the token project', (done) -> - expect( - @TokenAccessHandler.findProjectWithHigherAccess.callCount - ).to.equal 1 - done() - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should not call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 0 - done() - - it 'should redirect to the canonical project url', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith(302, "/project/#{@project._id}")).to.equal true - done() - - describe 'when higher access is not available', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null, true) - @TokenAccessHandler.findProjectWithHigherAccess = - sinon.stub() - .callsArgWith(2, null, null) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( - @readAndWriteToken - )).to.equal true - done() - - it 'should check if user has higher access to the token project', (done) -> - expect( - @TokenAccessHandler.findProjectWithHigherAccess.callCount - ).to.equal 1 - done() - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - describe 'when adding user to project produces an error', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, new Error('woops')) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith(@readAndWriteToken)) - .to.equal true - done() - - it 'should add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 1 - expect(@TokenAccessHandler.addReadAndWriteUserToProject.calledWith( - @userId.toString(), @projectId - )) - .to.equal true - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should call next with an error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - - describe 'readOnlyToken', -> - beforeEach -> - @TokenAccessHandler.checkV1Access = sinon.stub().callsArgWith(1, null, true) - - describe 'when access not allowed by v1 api', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @res.redirect = sinon.stub() - @next = sinon.stub() - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.getV1DocPublishedInfo = sinon.stub().yields(null, { - allow: false - published_path: 'doc-url' - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should redirect to doc-url', -> - expect(@res.redirect.calledWith('doc-url')).to.equal true - - describe 'with a user', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId.toString()) - - describe 'when all goes well', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_only_token'] = @readOnlyToken - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.calledWith(@readOnlyToken)) - .to.equal true - done() - - it 'should add the user to the project with read-only access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 1 - expect(@TokenAccessHandler.addReadOnlyUserToProject.calledWith( - @userId.toString(), @projectId - )) - .to.equal true - done() - - it 'should pass control to loadEditor', (done) -> - expect(@req.params.Project_id).to.equal @projectId.toString() - expect(@ProjectController.loadEditor.callCount).to.equal 1 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal true - done() - - describe 'when the user is already the owner', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_only_token'] = @readOnlyToken - @project.owner_ref = @userId - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.calledWith(@readOnlyToken)) - .to.equal true - done() - - it 'should not add the user to the project with read-only access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 0 - done() - - it 'should pass control to loadEditor', (done) -> - expect(@req.params.Project_id).to.equal @projectId.toString() - expect(@ProjectController.loadEditor.callCount).to.equal 1 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal true - done() - - describe 'when findProject produces an error', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_only_token'] = @readOnlyToken - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, new Error('woops')) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.calledWith(@readOnlyToken)) - .to.equal true - done() - - it 'should not add the user to the project with read-only access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should call next with an error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - describe 'when findProject does not find a project', -> - describe 'when project does not exist', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @res.redirect = sinon.stub() - @next = sinon.stub() - @req.params['read_only_token'] = 'abcd' - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null, false) - @TokenAccessHandler.checkV1ProjectExported = sinon.stub() - .callsArgWith(1, null, false) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should redirect to v1', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith( - 302, - '/sign_in_to_v1?return_to=/read/abcd' - )).to.equal true - done() - - describe 'when project was not exported from v1 but forcing import to v2', -> - beforeEach -> - @Features.hasFeature.returns(true) - @req = new MockRequest() - @res = new MockResponse() - @res.render = sinon.stub() - @next = sinon.stub() - @req.params['read_only_token'] = 'abcd' - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null, false) - - describe 'with project name', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: true - name: 'A title' - has_assignment: false - brand_info: null - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should render v2-import page with name', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: 'abcd' - name: 'A title' - hasOwner: true - hasAssignment: false - brandInfo: null - } - )).to.equal true - done() - - describe 'with project owner', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: true - name: 'A title' - has_assignment: false - brand_info: null - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: 'abcd', - hasOwner: true - name: 'A title' - hasAssignment: false - brandInfo: null - } - )).to.equal true - done() - - describe 'without project owner', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: false - name: 'A title' - has_assignment: false - brand_info: null - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: 'abcd', - hasOwner: false - name: 'A title' - hasAssignment: false - brandInfo: null - } - )).to.equal true - done() - - describe 'with assignment', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: false - name: 'A title' - has_assignment: true - brand_info: null - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: 'abcd', - hasOwner: false - name: 'A title' - hasAssignment: true - brandInfo: null - } - )).to.equal true - done() - - describe 'with brand info', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - has_owner: false - name: 'A title' - has_assignment: false - brand_info: 'f1000' - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should render v2-import page', (done) -> - expect(@res.render.calledWith( - 'project/v2-import', - { - projectId: 'abcd', - hasOwner: false - name: 'A title' - hasAssignment: false - brandInfo: 'f1000' - } - )).to.equal true - done() - - describe 'when project was exported from v1', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @res.redirect = sinon.stub() - @next = sinon.stub() - @req.params['read_only_token'] = 'abcd' - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null, false) - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - allow: true - exists: true - exported: true - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 1 - done() - - describe 'when token access is off, but user has higher access anyway', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @res.redirect = sinon.stub() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null, true) - @TokenAccessHandler.findProjectWithHigherAccess = - sinon.stub() - .callsArgWith(2, null, @project) - @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith(@readAndWriteToken)) - .to.equal true - done() - - it 'should check if user has higher access to the token project', (done) -> - expect( - @TokenAccessHandler.findProjectWithHigherAccess.callCount - ).to.equal 1 - done() - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadAndWriteUserToProject.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should not call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 0 - done() - - it 'should redirect to the canonical project url', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith(302, "/project/#{@project._id}")).to.equal true - done() - - describe 'when higher access is not available', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_and_write_token'] = @readAndWriteToken - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null, true) - @TokenAccessHandler.findProjectWithHigherAccess = - sinon.stub() - .callsArgWith(2, null, null) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readAndWriteToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( - @readAndWriteToken - )).to.equal true - done() - - it 'should check if user has higher access to the token project', (done) -> - expect( - @TokenAccessHandler.findProjectWithHigherAccess.callCount - ).to.equal 1 - done() - - it 'should not add the user to the project with read-write access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - describe 'when adding user to project produces an error', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_only_token'] = @readOnlyToken - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - .callsArgWith(2, new Error('woops')) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.calledWith(@readOnlyToken)) - .to.equal true - done() - - it 'should add the user to the project with read-only access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 1 - expect(@TokenAccessHandler.addReadOnlyUserToProject.calledWith( - @userId.toString(), @projectId - )) - .to.equal true - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should call next with an error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - describe 'anonymous', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(null) - @TokenAccessHandler.grantSessionTokenAccess = sinon.stub() - - describe 'when all goes well', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_only_token'] = @readOnlyToken - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project, true) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.calledWith(@readOnlyToken)) - .to.equal true - done() - - it 'should give the user session read-only access', (done) -> - expect(@TokenAccessHandler.grantSessionTokenAccess.callCount) - .to.equal 1 - expect(@TokenAccessHandler.grantSessionTokenAccess.calledWith( - @req, @projectId, @readOnlyToken - )) - .to.equal true - done() - - it 'should not add the user to the project with read-only access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 0 - done() - - it 'should pass control to loadEditor', (done) -> - expect(@req.params.Project_id).to.equal @projectId.toString() - expect(@req._anonymousAccessToken).to.equal @readOnlyToken - expect(@ProjectController.loadEditor.callCount).to.equal 1 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal true - done() - - describe 'when findProject produces an error', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - @req.params['read_only_token'] = @readOnlyToken - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, new Error('woops')) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - .callsArgWith(2, null) - @ProjectController.loadEditor = sinon.stub() - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.calledWith(@readOnlyToken)) - .to.equal true - done() - - it 'should not give the user session read-only access', (done) -> - expect(@TokenAccessHandler.grantSessionTokenAccess.callCount) - .to.equal 0 - done() - - it 'should not add the user to the project with read-only access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 0 - done() - - it 'should not pass control to loadEditor', (done) -> - expect(@ProjectController.loadEditor.callCount).to.equal 0 - expect(@ProjectController.loadEditor.calledWith(@req, @res, @next)).to.equal false - done() - - it 'should call next with an error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() - - describe 'when findProject does not find a project', -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @res.redirect = sinon.stub() - @next = sinon.stub() - @req.params['read_only_token'] = @readOnlyToken - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId.toString()) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, false) - @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() - - describe 'when project does not exist', -> - beforeEach -> - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should try to find a project with this token', (done) -> - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.calledWith(@readOnlyToken)) - .to.equal true - done() - - it 'should not give the user session read-only access', (done) -> - expect(@TokenAccessHandler.grantSessionTokenAccess.callCount) - .to.equal 0 - done() - - it 'should not add the user to the project with read-only access', (done) -> - expect(@TokenAccessHandler.addReadOnlyUserToProject.callCount) - .to.equal 0 - done() - - describe 'when project was exported to v2', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: true - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should call next with not found error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.calledWith(new Errors.NotFoundError())).to.equal true - done() - - describe 'when project was not exported to v2', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should redirect to v1', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith( - 302, - "/sign_in_to_v1?return_to=/read/#{@readOnlyToken}" - )).to.equal true - done() - - describe 'when project does not exist on v1', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: false, - exported: false - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should call next with not found error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.calledWith(new Errors.NotFoundError())).to.equal true - done() - - describe 'anonymous user', -> - beforeEach -> - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(null) - - describe 'when project was not exported to v2', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should redirect to v1', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.calledWith( - 302, - "/sign_in_to_v1?return_to=/read/#{@readOnlyToken}" - )).to.equal true - done() - - describe 'force-import-to-v2 flag is on', -> - beforeEach -> - @res.render = sinon.stub() - @Features.hasFeature.returns(true) - - describe 'when project was not exported to v2', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { - exists: true - exported: false - }) - @TokenAccessController.readOnlyToken @req, @res, @next - - it 'should render anonymous import status page', (done) -> - expect(@res.render.callCount).to.equal 1 - expect(@res.render.calledWith( - 'project/v2-import', - { loginRedirect: "/read/#{@readOnlyToken}" } - )).to.equal true - done() - diff --git a/services/web/test/unit/coffee/TokenAccess/TokenAccessHandlerTests.coffee b/services/web/test/unit/coffee/TokenAccess/TokenAccessHandlerTests.coffee deleted file mode 100644 index f3dddb02a2..0000000000 --- a/services/web/test/unit/coffee/TokenAccess/TokenAccessHandlerTests.coffee +++ /dev/null @@ -1,611 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/TokenAccess/TokenAccessHandler" -expect = require("chai").expect -ObjectId = require("mongojs").ObjectId - -describe "TokenAccessHandler", -> - - beforeEach -> - @token = 'sometokenthing' - @projectId = ObjectId() - @project = - _id: @projectId - publicAccesLevel: 'tokenBased' - @userId = ObjectId() - @req = {} - @TokenAccessHandler = SandboxedModule.require modulePath, requires: - '../../models/Project': {Project: @Project = {}} - 'settings-sharelatex': @settings = {} - '../Collaborators/CollaboratorsHandler': @CollaboratorsHandler = {} - '../User/UserGetter': @UserGetter = {} - '../V1/V1Api': @V1Api = { - request: sinon.stub() - } - - describe 'findProjectWithReadOnlyToken', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - - it 'should call Project.findOne', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) => - expect(@Project.findOne.callCount).to.equal 1 - expect(@Project.findOne.calledWith({ - 'tokens.readOnly': @token - })).to.equal true - done() - - it 'should produce a project object with no error', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) => - expect(err).to.not.exist - expect(project).to.exist - expect(project).to.deep.equal @project - done() - - it 'should return projectExists flag as true', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project, projectExists) -> - expect(projectExists).to.equal true - done() - - describe 'when Project.findOne produces an error', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) => - expect(err).to.exist - expect(project).to.not.exist - expect(err).to.be.instanceof Error - done() - - describe 'when project does not have tokenBased access level', -> - beforeEach -> - @project.publicAccesLevel = 'private' - @Project.findOne = sinon.stub().callsArgWith(2, null, @project, true) - - it 'should not return a project', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) -> - expect(err).to.not.exist - expect(project).to.not.exist - done() - - it 'should return projectExists flag as true', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project, projectExists) -> - expect(projectExists).to.equal true - done() - - describe 'when project does not exist', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, null, null) - - it 'should not return a project', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) -> - expect(err).to.not.exist - expect(project).to.not.exist - done() - - it 'should return projectExists flag as false', (done) -> - @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project, projectExists) -> - expect(projectExists).to.equal false - done() - - describe 'findProjectWithReadAndWriteToken', -> - beforeEach -> - @token = '1234bcdf' - @tokenPrefix = '1234' - @project.tokens = { - readOnly: 'atntntn' - readAndWrite: @token, - readAndWritePrefix: @tokenPrefix - } - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - - it 'should call Project.findOne', (done) -> - @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project) => - expect(@Project.findOne.callCount).to.equal 1 - expect(@Project.findOne.calledWith({ - 'tokens.readAndWritePrefix': @tokenPrefix - })).to.equal true - done() - - it 'should produce a project object with no error', (done) -> - @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project) => - expect(err).to.not.exist - expect(project).to.exist - expect(project).to.deep.equal @project - done() - - it 'should return projectExists flag as true', (done) -> - @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project, projectExists) -> - expect(projectExists).to.equal true - done() - - describe 'when Project.findOne produces an error', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project) => - expect(err).to.exist - expect(project).to.not.exist - expect(err).to.be.instanceof Error - done() - - describe 'when project does not have tokenBased access level', -> - beforeEach -> - @project.publicAccesLevel = 'private' - @Project.findOne = sinon.stub().callsArgWith(2, null, @project, true) - - it 'should not return a project', (done) -> - @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project) -> - expect(err).to.not.exist - expect(project).to.not.exist - done() - - it 'should return projectExists flag as true', (done) -> - @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project, projectExists) -> - expect(projectExists).to.equal true - done() - - describe 'when the tokens have different lengths', -> - beforeEach -> - @project.tokens = { - readOnly: 'atntntn' - readAndWrite: @token + "some-other-characters", - readAndWritePrefix: @tokenPrefix - } - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - - it 'should not return a project', (done) -> - @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project) -> - expect(err).to.not.exist - expect(project).to.not.exist - done() - - describe 'findProjectWithHigherAccess', -> - describe 'when user does have higher access', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - @CollaboratorsHandler.isUserInvitedMemberOfProject = sinon.stub() - .callsArgWith(2, null, true) - - it 'should call Project.findOne', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(@Project.findOne.callCount).to.equal 1 - expect(@Project.findOne.calledWith({ - 'tokens.readOnly': @token - })).to.equal true - done() - - it 'should call isUserInvitedMemberOfProject', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(@CollaboratorsHandler.isUserInvitedMemberOfProject.callCount) - .to.equal 1 - expect(@CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith( - @userId, @project._id - )).to.equal true - done() - - it 'should produce a project object', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(err).to.not.exist - expect(project).to.exist - expect(project).to.deep.equal @project - done() - - describe 'when user does not have higher access', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - @CollaboratorsHandler.isUserInvitedMemberOfProject = sinon.stub() - .callsArgWith(2, null, false) - - it 'should call Project.findOne', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(@Project.findOne.callCount).to.equal 1 - expect(@Project.findOne.calledWith({ - 'tokens.readOnly': @token - })).to.equal true - done() - - it 'should call isUserInvitedMemberOfProject', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(@CollaboratorsHandler.isUserInvitedMemberOfProject.callCount) - .to.equal 1 - expect(@CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith( - @userId, @project._id - )).to.equal true - done() - - it 'should not produce a project', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(err).to.not.exist - expect(project).to.not.exist - done() - - describe 'when Project.findOne produces an error', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(err).to.exist - expect(project).to.not.exist - expect(err).to.be.instanceof Error - done() - - describe 'when isUserInvitedMemberOfProject produces an error', -> - beforeEach -> - @Project.findOne = sinon.stub().callsArgWith(2, null, @project) - @CollaboratorsHandler.isUserInvitedMemberOfProject = sinon.stub() - .callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @TokenAccessHandler.findProjectWithHigherAccess @token, @userId, (err, project) => - expect(err).to.exist - expect(project).to.not.exist - expect(err).to.be.instanceof Error - done() - - describe 'addReadOnlyUserToProject', -> - beforeEach -> - @Project.update = sinon.stub().callsArgWith(2, null) - - it 'should call Project.update', (done) -> - @TokenAccessHandler.addReadOnlyUserToProject @userId, @projectId, (err) => - expect(@Project.update.callCount).to.equal 1 - expect(@Project.update.calledWith({ - _id: @projectId - })).to.equal true - expect(@Project.update.lastCall.args[1]['$addToSet']) - .to.have.keys 'tokenAccessReadOnly_refs' - done() - - it 'should not produce an error', (done) -> - @TokenAccessHandler.addReadOnlyUserToProject @userId, @projectId, (err) => - expect(err).to.not.exist - done() - - describe 'when Project.update produces an error', -> - beforeEach -> - @Project.update = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @TokenAccessHandler.addReadOnlyUserToProject @userId, @projectId, (err) => - expect(err).to.exist - done() - - - describe 'addReadAndWriteUserToProject', -> - beforeEach -> - @Project.update = sinon.stub().callsArgWith(2, null) - - it 'should call Project.update', (done) -> - @TokenAccessHandler.addReadAndWriteUserToProject @userId, @projectId, (err) => - expect(@Project.update.callCount).to.equal 1 - expect(@Project.update.calledWith({ - _id: @projectId - })).to.equal true - expect(@Project.update.lastCall.args[1]['$addToSet']) - .to.have.keys 'tokenAccessReadAndWrite_refs' - done() - - it 'should not produce an error', (done) -> - @TokenAccessHandler.addReadAndWriteUserToProject @userId, @projectId, (err) => - expect(err).to.not.exist - done() - - describe 'when Project.update produces an error', -> - beforeEach -> - @Project.update = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @TokenAccessHandler.addReadAndWriteUserToProject @userId, @projectId, (err) => - expect(err).to.exist - done() - - - describe 'grantSessionTokenAccess', -> - beforeEach -> - @req = {session: {}, headers: {}} - - it 'should add the token to the session', (done) -> - @TokenAccessHandler.grantSessionTokenAccess(@req, @projectId, @token) - expect(@req.session.anonTokenAccess[@projectId.toString()]) - .to.equal @token - done() - - - - - - - - describe 'isValidToken', -> - - describe 'when a read-only project is found', -> - - beforeEach -> - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project) - - it 'should try to find projects with both kinds of token', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - done() - - it 'should allow read-only access', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) => - expect(err).to.not.exist - expect(rw).to.equal false - expect(ro).to.equal true - done() - - describe 'when a read-and-write project is found', -> - - beforeEach -> - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null) - - it 'should try to find projects with both kinds of token', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - done() - - it 'should allow read-and-write access', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) => - expect(err).to.not.exist - expect(rw).to.equal true - expect(ro).to.equal false - done() - - describe 'when no project is found', -> - beforeEach -> - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null) - - it 'should try to find projects with both kinds of token', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - done() - - it 'should not allow any access', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) => - expect(err).to.not.exist - expect(rw).to.equal false - expect(ro).to.equal false - done() - - describe 'when findProject produces an error', -> - beforeEach -> - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, new Error('woops')) - - it 'should try to find projects with both kinds of token', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - done() - - it 'should produce an error and not allow access', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) => - expect(err).to.exist - expect(err).to.be.instanceof Error - expect(rw).to.equal undefined - expect(ro).to.equal undefined - done() - - describe 'when project is not set to token-based access', -> - beforeEach -> - @project.publicAccesLevel = 'private' - - describe 'for read-and-write project', -> - beforeEach -> - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null) - - it 'should try to find projects with both kinds of token', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - done() - - it 'should not allow any access', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) => - expect(err).to.not.exist - expect(rw).to.equal false - expect(ro).to.equal false - done() - - describe 'for read-only project', -> - beforeEach -> - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project) - - it 'should try to find projects with both kinds of token', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 1 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 1 - done() - - it 'should not allow any access', (done) -> - @TokenAccessHandler.isValidToken @projectId, @token, (err, rw, ro) => - expect(err).to.not.exist - expect(rw).to.equal false - expect(ro).to.equal false - done() - - describe 'with nothing', -> - beforeEach -> - @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) - @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null) - - it 'should not call findProjectWithReadOnlyToken', (done) -> - @TokenAccessHandler.isValidToken @projectId, null, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 0 - done() - - it 'should try to find projects with both kinds of token', (done) -> - @TokenAccessHandler.isValidToken @projectId, null, (err, allowed) => - expect(@TokenAccessHandler.findProjectWithReadAndWriteToken.callCount) - .to.equal 0 - expect(@TokenAccessHandler.findProjectWithReadOnlyToken.callCount) - .to.equal 0 - done() - - it 'should not allow any access', (done) -> - @TokenAccessHandler.isValidToken @projectId, null, (err, rw, ro) => - expect(err).to.not.exist - expect(rw).to.equal false - expect(ro).to.equal false - done() - - describe 'protectTokens', -> - beforeEach -> - @project = {tokens: {readAndWrite: 'rw', readOnly: 'ro', readAndWritePrefix: 'pre'}} - - it 'should hide write token from read-only user', -> - @TokenAccessHandler.protectTokens(@project, 'readOnly') - expect(@project.tokens.readAndWrite).to.equal '' - expect(@project.tokens.readAndWritePrefix).to.equal '' - expect(@project.tokens.readOnly).to.equal 'ro' - - it 'should hide read token from read-write user', -> - @TokenAccessHandler.protectTokens(@project, 'readAndWrite') - expect(@project.tokens.readAndWrite).to.equal 'rw' - expect(@project.tokens.readOnly).to.equal '' - - it 'should leave tokens in place for owner', -> - @TokenAccessHandler.protectTokens(@project, 'owner') - expect(@project.tokens.readAndWrite).to.equal 'rw' - expect(@project.tokens.readOnly).to.equal 'ro' - - describe 'getDocPublishedInfo', -> - beforeEach -> - @callback = sinon.stub() - - describe 'when v1 api not set', -> - beforeEach -> - @TokenAccessHandler.getV1DocPublishedInfo @token, @callback - - it 'should not check access and return default info', -> - expect(@V1Api.request.called).to.equal false - expect(@callback.calledWith null, { - allow: true - }).to.equal true - - describe 'when v1 api is set', -> - beforeEach -> - @settings.apis = { v1: 'v1' } - - describe 'on V1Api.request success', -> - beforeEach -> - @V1Api.request = sinon.stub().callsArgWith(1, null, null, 'mock-data') - @TokenAccessHandler.getV1DocPublishedInfo @token, @callback - - it 'should return response body', -> - expect(@V1Api.request.calledWith { url: "/api/v1/sharelatex/docs/#{@token}/is_published" }).to.equal true - expect(@callback.calledWith null, 'mock-data').to.equal true - - describe 'on V1Api.request error', -> - beforeEach -> - @V1Api.request = sinon.stub().callsArgWith(1, 'error') - @TokenAccessHandler.getV1DocPublishedInfo @token, @callback - - it 'should callback with error', -> - expect(@callback.calledWith 'error').to.equal true - - describe 'getV1DocInfo', -> - beforeEach -> - @v2UserId = 123 - @callback = sinon.stub() - - describe 'when v1 api not set', -> - beforeEach -> - @TokenAccessHandler.getV1DocInfo @token, @v2UserId, @callback - - it 'should not check access and return default info', -> - expect(@V1Api.request.called).to.equal false - expect(@callback.calledWith null, { - exists: true - exported: false - }).to.equal true - - describe 'when v1 api is set', -> - beforeEach -> - @settings.apis = { v1: 'v1' } - - describe 'on UserGetter.getUser success', -> - beforeEach -> - @UserGetter.getUser = sinon.stub().yields(null, { - overleaf: { id: 1 } - }) - @TokenAccessHandler.getV1DocInfo @token, @v2UserId, @callback - - it 'should get user', -> - expect(@UserGetter.getUser.calledWith(@v2UserId)).to.equal true - - describe 'on UserGetter.getUser error', -> - beforeEach -> - @error = new Error('failed to get user') - @UserGetter.getUser = sinon.stub().yields(@error) - @TokenAccessHandler.getV1DocInfo @token, @v2UserId, @callback - - it 'should callback with error', -> - expect(@callback.calledWith @error).to.equal true - - describe 'on V1Api.request success', -> - beforeEach -> - @v1UserId = 1 - @UserGetter.getUser = sinon.stub().yields(null, { - overleaf: { id: @v1UserId } - }) - @V1Api.request = sinon.stub().callsArgWith(1, null, null, 'mock-data') - @TokenAccessHandler.getV1DocInfo @token, @v2UserId, @callback - - it 'should return response body', -> - expect(@V1Api.request.calledWith { url: "/api/v1/sharelatex/users/#{@v1UserId}/docs/#{@token}/info" }).to.equal true - expect(@callback.calledWith null, 'mock-data').to.equal true - - describe 'on V1Api.request error', -> - beforeEach -> - @UserGetter.getUser = sinon.stub().yields(null, { - overleaf: { id: 1 } - }) - @V1Api.request = sinon.stub().callsArgWith(1, 'error') - @TokenAccessHandler.getV1DocInfo @token, @v2UserId, @callback - - it 'should callback with error', -> - expect(@callback.calledWith 'error').to.equal true diff --git a/services/web/test/unit/coffee/Uploads/ArchiveManagerTests.coffee b/services/web/test/unit/coffee/Uploads/ArchiveManagerTests.coffee deleted file mode 100644 index 4c0208d559..0000000000 --- a/services/web/test/unit/coffee/Uploads/ArchiveManagerTests.coffee +++ /dev/null @@ -1,325 +0,0 @@ -sinon = require('sinon') -expect = require("chai").expect -chai = require('chai') -should = chai.should() -modulePath = "../../../../app/js/Features/Uploads/ArchiveManager.js" -Errors = require("../../../../app/js/Features/Errors/Errors") -SandboxedModule = require('sandboxed-module') -events = require "events" - -describe "ArchiveManager", -> - beforeEach -> - @logger = - error: sinon.stub() - warn: sinon.stub() - err:-> - log: sinon.stub() - @metrics = - Timer: class Timer - done: sinon.stub() - @zipfile = new events.EventEmitter - @zipfile.readEntry = sinon.stub() - @zipfile.close = sinon.stub() - - @ArchiveManager = SandboxedModule.require modulePath, requires: - "yauzl": @yauzl = {open: sinon.stub().callsArgWith(2, null, @zipfile)} - "logger-sharelatex": @logger - "metrics-sharelatex": @metrics - "fs": @fs = {} - "fs-extra": @fse = {} - @callback = sinon.stub() - - describe "extractZipArchive", -> - beforeEach -> - @source = "/path/to/zip/source.zip" - @destination = "/path/to/zip/destination" - @ArchiveManager._isZipTooLarge = sinon.stub().callsArgWith(1, null, false) - - describe "successfully", -> - beforeEach (done) -> - @ArchiveManager.extractZipArchive @source, @destination, done - @zipfile.emit "end" - - it "should run yauzl", -> - @yauzl.open.calledWith(@source).should.equal true - - it "should time the unzip", -> - @metrics.Timer::done.called.should.equal true - - it "should log the unzip", -> - @logger.log.calledWith(sinon.match.any, "unzipping file").should.equal true - - describe "with an error in the zip file header", -> - beforeEach (done) -> - @yauzl.open = sinon.stub().callsArgWith(2, new Errors.InvalidError("invalid_zip_file")) - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - - it "should return the callback with an error", -> - sinon.assert.calledWithExactly(@callback, new Errors.InvalidError("invalid_zip_file")) - - it "should log out the error", -> - @logger.error.called.should.equal true - - describe "with a zip that is too large", -> - beforeEach (done) -> - @ArchiveManager._isZipTooLarge = sinon.stub().callsArgWith(1, null, true) - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - - it "should return the callback with an error", -> - sinon.assert.calledWithExactly(@callback, new Errors.InvalidError("zip_contents_too_large")) - - it "should not call yauzl.open", -> - @yauzl.open.called.should.equal false - - describe "with an error in the extracted files", -> - beforeEach (done) -> - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "error", new Error("Something went wrong") - - it "should return the callback with an error", -> - @callback.calledWithExactly(new Error("Something went wrong")).should.equal true - - it "should log out the error", -> - @logger.error.called.should.equal true - - describe "with a relative extracted file path", -> - beforeEach (done) -> - @zipfile.openReadStream = sinon.stub() - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "entry", {fileName: "../testfile.txt"} - @zipfile.emit "end" - - it "should not write try to read the file entry", -> - @zipfile.openReadStream.called.should.equal false - - it "should log out a warning", -> - @logger.warn.called.should.equal true - - describe "with an unnormalized extracted file path", -> - beforeEach (done) -> - @zipfile.openReadStream = sinon.stub() - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "entry", {fileName: "foo/./testfile.txt"} - @zipfile.emit "end" - - it "should not try to read the file entry", -> - @zipfile.openReadStream.called.should.equal false - - it "should log out a warning", -> - @logger.warn.called.should.equal true - - describe "with backslashes in the path", -> - beforeEach (done) -> - @readStream = new events.EventEmitter - @readStream.pipe = sinon.stub() - @writeStream = new events.EventEmitter - @fs.createWriteStream = sinon.stub().returns @writeStream - @zipfile.openReadStream = sinon.stub().callsArgWith(1, null, @readStream) - @fse.ensureDir = sinon.stub().callsArg(1) - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "entry", {fileName: 'wombat\\foo.tex'} - @zipfile.emit "entry", {fileName: 'potato\\bar.tex'} - @zipfile.emit "end" - - it "should read the file entry with its original path", -> - @zipfile.openReadStream.should.be.calledWith({fileName: 'wombat\\foo.tex'}) - @zipfile.openReadStream.should.be.calledWith({fileName: 'potato\\bar.tex'}) - - it "should treat the backslashes as a directory separator when creating the directory", -> - @fse.ensureDir.should.be.calledWith("#{@destination}/wombat"); - @fse.ensureDir.should.be.calledWith("#{@destination}/potato"); - - it "should treat the backslashes as a directory separator when creating the file", -> - @fs.createWriteStream.should.be.calledWith("#{@destination}/wombat/foo.tex"); - @fs.createWriteStream.should.be.calledWith("#{@destination}/potato/bar.tex"); - - describe "with a directory entry", -> - beforeEach (done) -> - @zipfile.openReadStream = sinon.stub() - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "entry", {fileName: "testdir/"} - @zipfile.emit "end" - - it "should not try to read the entry", -> - @zipfile.openReadStream.called.should.equal false - - it "should not log out a warning", -> - @logger.warn.called.should.equal false - - describe "with an error opening the file read stream", -> - beforeEach (done) -> - @zipfile.openReadStream = sinon.stub().callsArgWith(1, new Error("Something went wrong")) - @writeStream = new events.EventEmitter - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "entry", {fileName: "testfile.txt"} - @zipfile.emit "end" - - it "should return the callback with an error", -> - @callback.calledWithExactly(new Error("Something went wrong")).should.equal true - - it "should log out the error", -> - @logger.error.called.should.equal true - - it "should close the zipfile", -> - @zipfile.close.called.should.equal true - - describe "with an error in the file read stream", -> - beforeEach (done) -> - @readStream = new events.EventEmitter - @readStream.pipe = sinon.stub() - @zipfile.openReadStream = sinon.stub().callsArgWith(1, null, @readStream) - @writeStream = new events.EventEmitter - @fs.createWriteStream = sinon.stub().returns @writeStream - @fse.ensureDir = sinon.stub().callsArg(1) - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "entry", {fileName: "testfile.txt"} - @readStream.emit "error", new Error("Something went wrong") - @zipfile.emit "end" - - it "should return the callback with an error", -> - @callback.calledWithExactly(new Error("Something went wrong")).should.equal true - - it "should log out the error", -> - @logger.error.called.should.equal true - - it "should close the zipfile", -> - @zipfile.close.called.should.equal true - - describe "with an error in the file write stream", -> - beforeEach (done) -> - @readStream = new events.EventEmitter - @readStream.pipe = sinon.stub() - @readStream.unpipe = sinon.stub() - @readStream.destroy = sinon.stub() - @zipfile.openReadStream = sinon.stub().callsArgWith(1, null, @readStream) - @writeStream = new events.EventEmitter - @fs.createWriteStream = sinon.stub().returns @writeStream - @fse.ensureDir = sinon.stub().callsArg(1) - @ArchiveManager.extractZipArchive @source, @destination, (error) => - @callback(error) - done() - @zipfile.emit "entry", {fileName: "testfile.txt"} - @writeStream.emit "error", new Error("Something went wrong") - @zipfile.emit "end" - - it "should return the callback with an error", -> - @callback.calledWithExactly(new Error("Something went wrong")).should.equal true - - it "should log out the error", -> - @logger.error.called.should.equal true - - it "should unpipe from the readstream", -> - @readStream.unpipe.called.should.equal true - - it "should destroy the readstream", -> - @readStream.destroy.called.should.equal true - - it "should close the zipfile", -> - @zipfile.close.called.should.equal true - - describe "_isZipTooLarge", -> - - it "should return false with small output", (done)-> - @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => - isTooLarge.should.equal false - done() - @zipfile.emit "entry", {uncompressedSize: 109042} - @zipfile.emit "end" - - it "should return true with large bytes", (done)-> - @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => - isTooLarge.should.equal true - done() - @zipfile.emit "entry", {uncompressedSize: 1090000000000000042} - @zipfile.emit "end" - - it "should return error on no data", (done)-> - @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => - expect(error).to.exist - done() - @zipfile.emit "entry", {} - @zipfile.emit "end" - - it "should return error if it didn't get a number", (done)-> - @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => - expect(error).to.exist - done() - @zipfile.emit "entry", {uncompressedSize:"random-error"} - @zipfile.emit "end" - - it "should return error if there is no data", (done)-> - @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => - expect(error).to.exist - done() - @zipfile.emit "end" - - describe "findTopLevelDirectory", -> - beforeEach -> - @fs.readdir = sinon.stub() - @fs.stat = sinon.stub() - @directory = "test/directory" - - describe "with multiple files", -> - beforeEach -> - @fs.readdir.callsArgWith(1, null, ["multiple", "files"]) - @ArchiveManager.findTopLevelDirectory(@directory, @callback) - - it "should find the files in the directory", -> - @fs.readdir - .calledWith(@directory) - .should.equal true - - it "should return the original directory", -> - @callback - .calledWith(null, @directory) - .should.equal true - - describe "with a single file (not folder)", -> - beforeEach -> - @fs.readdir.callsArgWith(1, null, ["foo.tex"]) - @fs.stat.callsArgWith(1, null, { isDirectory: () -> false }) - @ArchiveManager.findTopLevelDirectory(@directory, @callback) - - it "should check if the file is a directory", -> - @fs.stat - .calledWith(@directory + "/foo.tex") - .should.equal true - - it "should return the original directory", -> - @callback - .calledWith(null, @directory) - .should.equal true - - describe "with a single top-level folder", -> - beforeEach -> - @fs.readdir.callsArgWith(1, null, ["folder"]) - @fs.stat.callsArgWith(1, null, { isDirectory: () -> true }) - @ArchiveManager.findTopLevelDirectory(@directory, @callback) - - it "should check if the file is a directory", -> - @fs.stat - .calledWith(@directory + "/folder") - .should.equal true - - it "should return the child directory", -> - @callback - .calledWith(null, @directory + "/folder") - .should.equal true diff --git a/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee b/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee deleted file mode 100644 index 80bf69e13d..0000000000 --- a/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee +++ /dev/null @@ -1,216 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -modulePath = "../../../../app/js/Features/Uploads/FileSystemImportManager.js" -SandboxedModule = require('sandboxed-module') - -describe "FileSystemImportManager", -> - beforeEach -> - @project_id = "project-id-123" - @folder_id = "folder-id-123" - @name = "test-file.tex" - @path_on_disk = "/path/to/file/#{@name}" - @replace = "replace-boolean-flag-mock" - @user_id = "mock-user-123" - @callback = sinon.stub() - @encoding = "latin1" - @DocumentHelper = - convertTexEncodingsToUtf8: sinon.stub().returnsArg(0) - @FileSystemImportManager = SandboxedModule.require modulePath, requires: - "fs" : @fs = {} - "../Editor/EditorController": @EditorController = {} - "./FileTypeManager": @FileTypeManager = {} - "../Project/ProjectLocator": @ProjectLocator = {} - "../Documents/DocumentHelper": @DocumentHelper - "logger-sharelatex": - log:-> - err:-> - - describe "addDoc", -> - beforeEach -> - @docContent = "one\ntwo\nthree" - @docLines = @docContent.split("\n") - @fs.readFile = sinon.stub().callsArgWith(2, null, @docContent) - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - - - describe "when path is symlink", -> - beforeEach -> - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, false) - @EditorController.addDoc = sinon.stub() - @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, @encoding, false, @callback - - it "should not read the file from disk", -> - @fs.readFile.called.should.equal false - - it "should not insert the doc", -> - @EditorController.addDoc.called.should.equal false - - describe "with replace set to false", -> - beforeEach -> - @EditorController.addDoc = sinon.stub().callsArg(6) - @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, @encoding, false, @callback - - it "should read the file from disk", -> - @fs.readFile.calledWith(@path_on_disk).should.equal true - - it "should insert the doc", -> - @EditorController.addDoc.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) - .should.equal true - - describe "with windows line ending", -> - beforeEach -> - @docContent = "one\r\ntwo\r\nthree" - @docLines = ["one", "two", "three"] - @fs.readFile = sinon.stub().callsArgWith(2, null, @docContent) - @EditorController.addDoc = sinon.stub().callsArg(6) - @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, @encoding, false, @callback - - it "should strip the \\r characters before adding", -> - @EditorController.addDoc.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) - .should.equal true - - describe "with \r line endings", -> - beforeEach -> - @docContent = "one\rtwo\rthree" - @docLines = ["one", "two", "three"] - @fs.readFile = sinon.stub().callsArgWith(2, null, @docContent) - @EditorController.addDoc = sinon.stub().callsArg(6) - @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, @encoding, false, @callback - - it "should treat the \\r characters as newlines", -> - @EditorController.addDoc.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) - .should.equal true - - describe "with replace set to true", -> - beforeEach -> - @EditorController.upsertDoc = sinon.stub().yields() - @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, @encoding, true, @callback - - it "should upsert the doc", -> - @EditorController.upsertDoc - .calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) - .should.equal true - - it "should read the file with the correct encoding", -> - sinon.assert.calledWith(@fs.readFile, @path_on_disk, @encoding) - - describe "addFile with replace set to false", -> - beforeEach -> - @EditorController.addFile = sinon.stub().yields() - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback - - it "should add the file", -> - @EditorController.addFile.calledWith(@project_id, @folder_id, @name, @path_on_disk, null, "upload", @user_id) - .should.equal true - - describe "addFile with symlink", -> - beforeEach -> - @EditorController.addFile = sinon.stub() - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, false) - @EditorController.replaceFile = sinon.stub() - @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback - - it "should node add the file", -> - @EditorController.addFile.called.should.equal false - @EditorController.replaceFile.called.should.equal false - - describe "addFile with replace set to true", -> - beforeEach -> - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @EditorController.upsertFile = sinon.stub().yields() - @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback - - it "should add the file", -> - @EditorController.upsertFile - .calledWith(@project_id, @folder_id, @name, @path_on_disk, null, "upload", @user_id) - .should.equal true - - describe "addFolder", -> - - beforeEach -> - @new_folder_id = "new-folder-id" - @EditorController.addFolder = sinon.stub().callsArgWith(4, null, _id: @new_folder_id) - @FileSystemImportManager.addFolderContents = sinon.stub().callsArg(5) - - describe "successfully", -> - beforeEach -> - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @FileSystemImportManager.addFolder @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback - - it "should add a folder to the project", -> - @EditorController.addFolder.calledWith(@project_id, @folder_id, @name, "upload") - .should.equal true - - it "should add the folders contents", -> - @FileSystemImportManager.addFolderContents.calledWith(@user_id, @project_id, @new_folder_id, @path_on_disk, @replace) - .should.equal true - - describe "with symlink", -> - beforeEach -> - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, false) - @FileSystemImportManager.addFolder @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback - - it "should not add a folder to the project", -> - @EditorController.addFolder.called.should.equal false - @FileSystemImportManager.addFolderContents.called.should.equal false - - describe "addFolderContents", -> - beforeEach -> - @folderEntries = ["path1", "path2", "path3"] - @ignoredEntries = [".DS_Store"] - @fs.readdir = sinon.stub().callsArgWith(1, null, @folderEntries.concat @ignoredEntries) - @FileSystemImportManager.addEntity = sinon.stub().callsArg(6) - @FileTypeManager.shouldIgnore = (path, callback) => - callback null, @ignoredEntries.indexOf(require("path").basename(path)) != -1 - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @FileSystemImportManager.addFolderContents @user_id, @project_id, @folder_id, @path_on_disk, @replace, @callback - - it "should call addEntity for each file in the folder which is not ignored", -> - for name in @folderEntries - @FileSystemImportManager.addEntity.calledWith(@user_id, @project_id, @folder_id, name, "#{@path_on_disk}/#{name}", @replace) - .should.equal true - - it "should not call addEntity for the ignored files", -> - for name in @ignoredEntries - @FileSystemImportManager.addEntity.calledWith(@user_id, @project_id, @folder_id, name, "#{@path_on_disk}/#{name}", @replace) - .should.equal false - - it "should look in the correct directory", -> - @fs.readdir.calledWith(@path_on_disk).should.equal true - - describe "addEntity", -> - describe "with directory", -> - beforeEach -> - @FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, true) - @FileSystemImportManager.addFolder = sinon.stub().callsArg(6) - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @FileSystemImportManager.addEntity @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback - - it "should call addFolder", -> - @FileSystemImportManager.addFolder.calledWith(@user_id, @project_id, @folder_id, @name, @path_on_disk, @replace) - .should.equal true - - describe "with binary file", -> - beforeEach -> - @FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, false) - @FileTypeManager.getType = sinon.stub().callsArgWith(2, null, true) - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @FileSystemImportManager.addFile = sinon.stub().callsArg(6) - @FileSystemImportManager.addEntity @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback - - it "should call addFile", -> - @FileSystemImportManager.addFile.calledWith(@user_id, @project_id, @folder_id, @name, @path_on_disk, @replace) - .should.equal true - - describe "with text file", -> - beforeEach -> - @FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, false) - @FileTypeManager.getType = sinon.stub().callsArgWith(2, null, false, 'latin1') - @FileSystemImportManager.addDoc = sinon.stub().callsArg(7) - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @FileSystemImportManager.addEntity @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback - - it "should call addFile", -> - sinon.assert.calledWith(@FileSystemImportManager.addDoc, @user_id, @project_id, @folder_id, @name, @path_on_disk, "latin1", @replace) diff --git a/services/web/test/unit/coffee/Uploads/FileTypeManagerTests.coffee b/services/web/test/unit/coffee/Uploads/FileTypeManagerTests.coffee deleted file mode 100644 index 232b29c9dd..0000000000 --- a/services/web/test/unit/coffee/Uploads/FileTypeManagerTests.coffee +++ /dev/null @@ -1,177 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -modulePath = "../../../../app/js/Features/Uploads/FileTypeManager.js" -SandboxedModule = require('sandboxed-module') -isUtf8 = require('is-utf8') - -describe "FileTypeManager", -> - beforeEach -> - @isUtf8 = sinon.spy(isUtf8) - @fs = {} - @path = "/path/to/test" - @callback = sinon.stub() - @ced = sinon.stub() - @DocumentHelper = - getEncodingFromTexContent: sinon.stub() - @FileTypeManager = SandboxedModule.require modulePath, requires: - "fs": @fs - "is-utf8": @isUtf8 - - describe "isDirectory", -> - beforeEach -> - @stats = {} - @fs.stat = sinon.stub().callsArgWith(1, null, @stats) - - describe "when it is a directory", -> - beforeEach -> - @stats.isDirectory = sinon.stub().returns true - @FileTypeManager.isDirectory @path, @callback - - it "should return true", -> - @callback.calledWith(null, true).should.equal true - - describe "when it is not a directory", -> - beforeEach -> - @stats.isDirectory = sinon.stub().returns false - @FileTypeManager.isDirectory @path, @callback - - it "should return false", -> - @callback.calledWith(null, false).should.equal true - - describe "getType", -> - beforeEach -> - @stat = { size: 100 } - @contents = "Ich bin eine kleine Teekanne, kurz und kräftig." - @fs.stat = sinon.stub().callsArgWith(1, null, @stat) - @fs.readFile = sinon.stub().callsArgWith(1, null, Buffer.from(@contents, "utf-8")) - @fs.readFile.withArgs("/path/on/disk/utf16.tex").callsArgWith(1, null, Buffer.from("\uFEFF" + @contents, "utf-16le")) - @fs.readFile.withArgs("/path/on/disk/latin1.tex").callsArgWith(1, null, Buffer.from(@contents, "latin1")) - @encoding = "ASCII" - - describe "when the file extension is text", -> - it "should return .tex files as not binary", -> - @FileTypeManager.getType "file.tex", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return .bib files as not binary", -> - @FileTypeManager.getType "file.bib", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return .bibtex files as not binary", -> - @FileTypeManager.getType "file.bibtex", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return .cls files as not binary", -> - @FileTypeManager.getType "file.cls", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return .sty files as not binary", -> - @FileTypeManager.getType "file.sty", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return .bst files as not binary", -> - @FileTypeManager.getType "file.bst", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return .latexmkrc file as not binary", -> - @FileTypeManager.getType ".latexmkrc", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return latexmkrc file as not binary", -> - @FileTypeManager.getType "latexmkrc", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return lbx file as not binary", -> - @FileTypeManager.getType "file.lbx", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return bbx file as not binary", -> - @FileTypeManager.getType "file.bbx", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return cbx file as not binary", -> - @FileTypeManager.getType "file.cbx", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return m file as not binary", -> - @FileTypeManager.getType "file.m", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should ignore the case of an extension", -> - @FileTypeManager.getType "file.TEX", "/path/on/disk", (error, binary) -> - binary.should.equal false - - it "should return large text files as binary", -> - @stat.size = 2 * 1024 * 1024 # 2Mb - @FileTypeManager.getType "file.tex", "/path/on/disk", (error, binary) -> - binary.should.equal true - - it "should return try to determine the encoding of large files", -> - @stat.size = 2 * 1024 * 1024 # 2Mb - @FileTypeManager.getType "file.tex", "/path/on/disk", => - sinon.assert.notCalled(@isUtf8) - - it "should detect the file as utf8", -> - @FileTypeManager.getType "file.tex", "/path/on/disk", (error, binary, encoding) => - sinon.assert.calledOnce(@isUtf8) - @isUtf8.returned(true).should.equal true - encoding.should.equal "utf-8" - - it "should return 'latin1' for non-unicode encodings", -> - @FileTypeManager.getType "file.tex", "/path/on/disk/latin1.tex", (error, binary, encoding) => - sinon.assert.calledOnce(@isUtf8) - @isUtf8.returned(false).should.equal true - encoding.should.equal "latin1" - - it "should detect utf16 with BOM as utf-16", -> - @FileTypeManager.getType "file.tex", "/path/on/disk/utf16.tex", (error, binary, encoding) => - sinon.assert.calledOnce(@isUtf8) - @isUtf8.returned(false).should.equal true - encoding.should.equal "utf-16le" - - describe "when the file extension is non-text", -> - it "should return .eps files as binary", -> - @FileTypeManager.getType "file.eps", "/path/on/disk", (error, binary) -> - binary.should.equal true - - it "should return .dvi files as binary", -> - @FileTypeManager.getType "file.dvi", "/path/on/disk", (error, binary) -> - binary.should.equal true - - it "should return .png files as binary", -> - @FileTypeManager.getType "file.png", "/path/on/disk", (error, binary) -> - binary.should.equal true - - it "should return files without extensions as binary", -> - @FileTypeManager.getType "tex", "/path/on/disk", (error, binary) -> - binary.should.equal true - - it "should not try to get the character encoding", -> - @FileTypeManager.getType "file.png", "/path/on/disk", => - sinon.assert.notCalled(@isUtf8) - - describe "shouldIgnore", -> - it "should ignore tex auxiliary files", -> - @FileTypeManager.shouldIgnore "file.aux", (error, ignore) -> - ignore.should.equal true - - it "should ignore dotfiles", -> - @FileTypeManager.shouldIgnore "path/.git", (error, ignore) -> - ignore.should.equal true - - it "should not ignore .latexmkrc dotfile", -> - @FileTypeManager.shouldIgnore "path/.latexmkrc", (error, ignore) -> - ignore.should.equal false - - it "should ignore __MACOSX", -> - @FileTypeManager.shouldIgnore "path/__MACOSX", (error, ignore) -> - ignore.should.equal true - - it "should not ignore .tex files", -> - @FileTypeManager.shouldIgnore "file.tex", (error, ignore) -> - ignore.should.equal false - - it "should ignore the case of the extension", -> - @FileTypeManager.shouldIgnore "file.AUX", (error, ignore) -> - ignore.should.equal true diff --git a/services/web/test/unit/coffee/Uploads/ProjectUploadControllerTests.coffee b/services/web/test/unit/coffee/Uploads/ProjectUploadControllerTests.coffee deleted file mode 100644 index b4065e5774..0000000000 --- a/services/web/test/unit/coffee/Uploads/ProjectUploadControllerTests.coffee +++ /dev/null @@ -1,189 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/Uploads/ProjectUploadController.js" -SandboxedModule = require('sandboxed-module') -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -Errors = require("../../../../app/js/Features/Errors/Errors") - -describe "ProjectUploadController", -> - beforeEach -> - @req = new MockRequest() - @res = new MockResponse() - @user_id = "user-id-123" - @metrics = - Timer: class Timer - done: sinon.stub() - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user_id) - - @ProjectUploadController = SandboxedModule.require modulePath, requires: - "./ProjectUploadManager" : @ProjectUploadManager = {} - "./FileSystemImportManager" : @FileSystemImportManager = {} - "logger-sharelatex" : @logger = {log: sinon.stub(), error: sinon.stub(), err:->} - "metrics-sharelatex": @metrics - '../Authentication/AuthenticationController': @AuthenticationController - "fs" : @fs = {} - - describe "uploadProject", -> - beforeEach -> - @path = "/path/to/file/on/disk.zip" - @name = "filename.zip" - @req.file = - path: @path - originalname: @name - @req.session = - user: - _id: @user_id - @project = - _id: @project_id = "project-id-123" - - @fs.unlink = sinon.stub() - - describe "successfully", -> - beforeEach -> - @ProjectUploadManager.createProjectFromZipArchive = - sinon.stub().callsArgWith(3, null, @project) - @ProjectUploadController.uploadProject @req, @res - - it "should create a project owned by the logged in user", -> - @ProjectUploadManager - .createProjectFromZipArchive - .calledWith(@user_id) - .should.equal true - - it "should create a project with the same name as the zip archive", -> - @ProjectUploadManager - .createProjectFromZipArchive - .calledWith(sinon.match.any, "filename", sinon.match.any) - .should.equal true - - it "should create a project from the zip archive", -> - @ProjectUploadManager - .createProjectFromZipArchive - .calledWith(sinon.match.any, sinon.match.any, @path) - .should.equal true - - it "should return a successful response to the FileUploader client", -> - expect(@res.body).to.deep.equal - success: true - project_id: @project_id - - it "should record the time taken to do the upload", -> - @metrics.Timer::done.called.should.equal true - - it "should output a log line", -> - @logger.log - .calledWith(sinon.match.any, "uploaded project") - .should.equal true - - it "should remove the uploaded file", -> - @fs.unlink.calledWith(@path).should.equal true - - describe "when ProjectUploadManager.createProjectFromZipArchive fails", -> - beforeEach -> - @ProjectUploadManager.createProjectFromZipArchive = - sinon.stub().callsArgWith(3, new Error("Something went wrong"), @project) - @ProjectUploadController.uploadProject @req, @res - - it "should return a failed response to the FileUploader client", -> - expect(@res.body).to.deep.equal JSON.stringify({ success: false, error: "upload_failed" }) - - it "should output an error log line", -> - @logger.error - .calledWith(sinon.match.any, "error uploading project") - .should.equal true - - describe "when ProjectUploadManager.createProjectFromZipArchive reports the file as invalid", -> - beforeEach -> - @ProjectUploadManager.createProjectFromZipArchive = - sinon.stub().callsArgWith(3, new Errors.InvalidError("zip_contents_too_large"), @project) - @ProjectUploadController.uploadProject @req, @res - - it "should return the reported error to the FileUploader client", -> - expect(@res.body).to.deep.equal JSON.stringify({ success: false, error: "zip_contents_too_large" }) - - it "should return an 'unprocessable entity' status code", -> - expect(@res.statusCode).to.equal 422 - - it "should output an error log line", -> - @logger.error - .calledWith(sinon.match.any, "error uploading project") - .should.equal true - - describe "uploadFile", -> - beforeEach -> - @project_id = "project-id-123" - @folder_id = "folder-id-123" - @path = "/path/to/file/on/disk.png" - @name = "filename.png" - @req.file = - path: @path - originalname: @name - @req.session = - user: - _id: @user_id - @req.params = - Project_id: @project_id - @req.query = - folder_id: @folder_id - @fs.unlink = sinon.stub() - - - describe "successfully", -> - - beforeEach -> - @entity = - _id : "1234" - type: 'file' - @FileSystemImportManager.addEntity = sinon.stub().callsArgWith(6, null, @entity) - @ProjectUploadController.uploadFile @req, @res - - it "should insert the file", -> - @FileSystemImportManager.addEntity - .calledWith(@user_id, @project_id, @folder_id, @name, @path) - .should.equal true - - it "should return a successful response to the FileUploader client", -> - expect(@res.body).to.deep.equal - success: true - entity_id: @entity._id - entity_type: 'file' - - it "should output a log line", -> - @logger.log - .calledWith(sinon.match.any, "uploaded file") - .should.equal true - - it "should time the request", -> - @metrics.Timer::done.called.should.equal true - - it "should remove the uploaded file", -> - @fs.unlink.calledWith(@path).should.equal true - - describe "when FileSystemImportManager.addEntity returns an error", -> - beforeEach -> - @FileSystemImportManager.addEntity = sinon.stub() - .callsArgWith(6, new Error("Sorry something went wrong")) - @ProjectUploadController.uploadFile @req, @res - - it "should return an unsuccessful response to the FileUploader client", -> - expect(@res.body).to.deep.equal - success: false - - it "should output an error log line", -> - @logger.error - .calledWith(sinon.match.any, "error uploading file") - .should.equal true - - describe "with a bad request", -> - - beforeEach -> - @req.file.originalname = "" - @ProjectUploadController.uploadFile @req, @res - - it "should return a a non success response", -> - expect(@res.body).to.deep.equal - success: false diff --git a/services/web/test/unit/coffee/Uploads/ProjectUploadManagerTests.coffee b/services/web/test/unit/coffee/Uploads/ProjectUploadManagerTests.coffee deleted file mode 100644 index ae9e94f7e3..0000000000 --- a/services/web/test/unit/coffee/Uploads/ProjectUploadManagerTests.coffee +++ /dev/null @@ -1,168 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -modulePath = "../../../../app/js/Features/Uploads/ProjectUploadManager.js" -SandboxedModule = require('sandboxed-module') - -describe "ProjectUploadManager", -> - beforeEach -> - @project_id = "project-id-123" - @folder_id = "folder-id-123" - @owner_id = "onwer-id-123" - @callback = sinon.stub() - @source = "/path/to/zip/file-name.zip" - @destination = "/path/to/zile/file-extracted" - @root_folder_id = @folder_id - @owner_id = "owner-id-123" - @name = "Project name" - @othername = "Other name" - @project = - _id: @project_id - rootFolder: [ _id: @root_folder_id ] - @ProjectUploadManager = SandboxedModule.require modulePath, requires: - "./FileSystemImportManager" : @FileSystemImportManager = {} - "./ArchiveManager" : @ArchiveManager = {} - "../Project/ProjectCreationHandler" : @ProjectCreationHandler = {} - "../Project/ProjectRootDocManager" : @ProjectRootDocManager = {} - "../Project/ProjectDetailsHandler" : @ProjectDetailsHandler = {} - "../Documents/DocumentHelper" : @DocumentHelper = {} - "rimraf" : @rimraf = sinon.stub().callsArg(1) - - @ArchiveManager.extractZipArchive = sinon.stub().callsArg(2) - @ArchiveManager.findTopLevelDirectory = sinon.stub().callsArgWith(1, null, @topLevelDestination = "/path/to/zip/file-extracted/nested") - @ProjectCreationHandler.createBlankProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArg(1) - @FileSystemImportManager.addFolderContents = sinon.stub().callsArg(5) - @ProjectRootDocManager.findRootDocFileFromDirectory = sinon.stub().callsArgWith(1, null, 'main.tex', @othername) - @ProjectRootDocManager.setRootDocFromName = sinon.stub().callsArg(2) - @DocumentHelper.getTitleFromTexContent = sinon.stub().returns(@othername) - @ProjectDetailsHandler.fixProjectName = sinon.stub().returnsArg(0) - - describe "createProjectFromZipArchive", -> - describe "when the title can be read from the root document", -> - beforeEach (done) -> - @ProjectUploadManager._getDestinationDirectory = sinon.stub().returns @destination - @ProjectDetailsHandler.generateUniqueName = sinon.stub().callsArgWith(2, null, @othername) - @ProjectUploadManager.createProjectFromZipArchive @owner_id, @name, @source, (err, project) => - @callback(err, project) - done() - - it "should set up the directory to extract the archive to", -> - @ProjectUploadManager._getDestinationDirectory.calledWith(@source).should.equal true - - it "should extract the archive", -> - @ArchiveManager.extractZipArchive.calledWith(@source, @destination).should.equal true - - it "should find the top level directory", -> - @ArchiveManager.findTopLevelDirectory.calledWith(@destination).should.equal true - - it "should insert the extracted archive into the folder", -> - @FileSystemImportManager.addFolderContents.calledWith(@owner_id, @project_id, @folder_id, @topLevelDestination, false) - .should.equal true - - it "should create a project owned by the owner_id", -> - @ProjectCreationHandler - .createBlankProject - .calledWith(@owner_id) - .should.equal true - - it "should create a project with the correct name", -> - @ProjectCreationHandler - .createBlankProject - .calledWith(sinon.match.any, @othername) - .should.equal true - - it "should read the title from the tex contents", -> - @DocumentHelper.getTitleFromTexContent.called.should.equal true - - it "should set the root document", -> - @ProjectRootDocManager.setRootDocFromName.calledWith(@project_id, 'main.tex').should.equal true - - it "should call the callback", -> - @callback.calledWith(sinon.match.falsy, @project).should.equal true - - it "should ensure the name is valid", -> - @ProjectDetailsHandler.fixProjectName.called.should.equal true - - describe "when the root document can't be determined", -> - beforeEach (done) -> - @ProjectRootDocManager.findRootDocFileFromDirectory = sinon.stub().callsArg(1) - @ProjectUploadManager._getDestinationDirectory = sinon.stub().returns @destination - @ProjectDetailsHandler.generateUniqueName = sinon.stub().callsArgWith(2, null, @name) - @ProjectUploadManager.createProjectFromZipArchive @owner_id, @name, @source, (err, project) => - @callback(err, project) - done() - - it "should not try to set the root doc", -> - @ProjectRootDocManager.setRootDocFromName.called.should.equal false - - describe "createProjectFromZipArchiveWithName", -> - beforeEach (done) -> - @ProjectDetailsHandler.generateUniqueName = sinon.stub().callsArgWith(2, null, @name) - @ProjectUploadManager.insertZipArchiveIntoFolder = sinon.stub().callsArg(4) - @ProjectUploadManager.createProjectFromZipArchiveWithName @owner_id, @name, @source, (err, project) => - @callback(err, project) - done() - - it "should create a project owned by the owner_id", -> - @ProjectCreationHandler - .createBlankProject - .calledWith(@owner_id) - .should.equal true - - it "should create a project with the correct name", -> - @ProjectCreationHandler - .createBlankProject - .calledWith(sinon.match.any, @name) - .should.equal true - - it "should insert the zip file contents into the root folder", -> - @ProjectUploadManager - .insertZipArchiveIntoFolder - .calledWith(@owner_id, @project_id, @root_folder_id, @source) - .should.equal true - - it "should automatically set the root doc", -> - @ProjectRootDocManager - .setRootDocAutomatically - .calledWith(@project_id) - .should.equal true - - it "should call the callback", -> - @callback.calledWith(sinon.match.falsy, @project).should.equal true - - describe "insertZipArchiveIntoFolder", -> - beforeEach (done) -> - @ProjectUploadManager._getDestinationDirectory = sinon.stub().returns @destination - @ProjectUploadManager.insertZipArchiveIntoFolder @owner_id, @project_id, @folder_id, @source, (err) => - @callback(err) - done() - - it "should set up the directory to extract the archive to", -> - @ProjectUploadManager._getDestinationDirectory.calledWith(@source).should.equal true - - it "should extract the archive", -> - @ArchiveManager.extractZipArchive.calledWith(@source, @destination).should.equal true - - it "should find the top level directory", -> - @ArchiveManager.findTopLevelDirectory.calledWith(@destination).should.equal true - - it "should insert the extracted archive into the folder", -> - @FileSystemImportManager.addFolderContents.calledWith(@owner_id, @project_id, @folder_id, @topLevelDestination, false) - .should.equal true - - it "should return the callback", -> - @callback.called.should.equal true - - it "should remove the desintation directory afterwards", -> - @rimraf.calledWith(@destination).should.equal true - - describe "_getDestinationDirectory", -> - it "should return the path with the time appended", -> - date = Date.now() - sinon.stub Date, "now", () -> date - @ProjectUploadManager - ._getDestinationDirectory("/path/to/zip/file.zip") - .should.equal "/path/to/zip/file-#{date}" - Date.now.restore() - diff --git a/services/web/test/unit/coffee/User/UserControllerTests.coffee b/services/web/test/unit/coffee/User/UserControllerTests.coffee deleted file mode 100644 index a9f41bfd24..0000000000 --- a/services/web/test/unit/coffee/User/UserControllerTests.coffee +++ /dev/null @@ -1,427 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/User/UserController.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -MockResponse = require "../helpers/MockResponse" -MockRequest = require "../helpers/MockRequest" -ObjectId = require("mongojs").ObjectId -assert = require("assert") -Errors = require "../../../../app/js/Features/Errors/Errors" - -describe "UserController", -> - beforeEach -> - @user_id = "323123" - - @user = - _id:@user_id - save: sinon.stub().callsArgWith(0) - ace:{} - - @req = - user: {} - session: - destroy:-> - user : - _id : @user_id - email:"old@something.com" - body:{} - - @UserDeleter = - deleteUser: sinon.stub().callsArgWith(1) - @UserGetter = - getUser: sinon.stub().callsArgWith(1, null, @user) - @User = - findById: sinon.stub().callsArgWith(1, null, @user) - @NewsLetterManager = - unsubscribe: sinon.stub().callsArgWith(1) - @UserRegistrationHandler = - registerNewUser: sinon.stub() - @AuthenticationController = - establishUserSession: sinon.stub().callsArg(2) - getLoggedInUserId: sinon.stub().returns(@user._id) - getSessionUser: sinon.stub().returns(@req.session.user) - setInSessionUser: sinon.stub() - @AuthenticationManager = - authenticate: sinon.stub() - setUserPassword: sinon.stub() - validatePassword: sinon.stub() - @ReferalAllocator = - allocate:sinon.stub() - @SubscriptionDomainHandler = - autoAllocate:sinon.stub() - @UserUpdater = - changeEmailAddress:sinon.stub() - @settings = - siteUrl: "sharelatex.example.com" - @UserHandler = - populateTeamInvites: sinon.stub().callsArgWith(1) - @UserSessionsManager = - trackSession: sinon.stub() - untrackSession: sinon.stub() - revokeAllUserSessions: sinon.stub().callsArgWith(2, null) - @SudoModeHandler = - clearSudoMode: sinon.stub() - @UserController = SandboxedModule.require modulePath, requires: - "./UserGetter": @UserGetter - "./UserDeleter": @UserDeleter - "./UserUpdater":@UserUpdater - "../../models/User": User:@User - '../Newsletter/NewsletterManager':@NewsLetterManager - "./UserRegistrationHandler":@UserRegistrationHandler - "../Authentication/AuthenticationController": @AuthenticationController - "../Authentication/AuthenticationManager": @AuthenticationManager - "../Referal/ReferalAllocator":@ReferalAllocator - "../Subscription/SubscriptionDomainHandler":@SubscriptionDomainHandler - "./UserHandler":@UserHandler - "./UserSessionsManager": @UserSessionsManager - "../SudoMode/SudoModeHandler": @SudoModeHandler - "settings-sharelatex": @settings - "logger-sharelatex": - log:-> - err:-> - "metrics-sharelatex": inc:-> - "../Errors/Errors": Errors - - @res = - send: sinon.stub() - sendStatus: sinon.stub() - json: sinon.stub() - @next = sinon.stub() - - describe 'tryDeleteUser', -> - - beforeEach -> - @req.body.password = 'wat' - @req.logout = sinon.stub() - @req.session.destroy = sinon.stub().callsArgWith(0, null) - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user._id) - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user) - @UserDeleter.deleteUser = sinon.stub().callsArgWith(1, null) - - it 'should send 200', (done) -> - @res.sendStatus = (code) => - code.should.equal 200 - done() - @UserController.tryDeleteUser @req, @res, @next - - it 'should try to authenticate user', (done) -> - @res.sendStatus = (code) => - @AuthenticationManager.authenticate.callCount.should.equal 1 - @AuthenticationManager.authenticate.calledWith({_id: @user._id}, @req.body.password).should.equal true - done() - @UserController.tryDeleteUser @req, @res, @next - - it 'should delete the user', (done) -> - @res.sendStatus = (code) => - @UserDeleter.deleteUser.callCount.should.equal 1 - @UserDeleter.deleteUser.calledWith(@user._id).should.equal true - done() - @UserController.tryDeleteUser @req, @res, @next - - describe 'when no password is supplied', -> - - beforeEach -> - @req.body.password = '' - - it 'should return 403', (done) -> - @res.sendStatus = (code) => - code.should.equal 403 - done() - @UserController.tryDeleteUser @req, @res, @next - - describe 'when authenticate produces an error', -> - - beforeEach -> - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, new Error('woops')) - - it 'should call next with an error', (done) -> - @next = (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - @UserController.tryDeleteUser @req, @res, @next - - describe 'when authenticate does not produce a user', -> - - beforeEach -> - @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null) - - it 'should return 403', (done) -> - @res.sendStatus = (code) => - code.should.equal 403 - done() - @UserController.tryDeleteUser @req, @res, @next - - describe 'when deleteUser produces an error', -> - - beforeEach -> - @UserDeleter.deleteUser = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should call next with an error', (done) -> - @next = (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - @UserController.tryDeleteUser @req, @res, @next - - describe 'when deleteUser produces a known error', -> - - beforeEach -> - @UserDeleter.deleteUser = sinon.stub().yields( - new Errors.SubscriptionAdminDeletionError() - ) - - it 'should return a json error', (done) -> - @UserController.tryDeleteUser @req, status: (status) -> - expect(status).to.equal 422 - json: (json) -> - expect(json.error).to.equal Errors.SubscriptionAdminDeletionError.name - done() - - describe 'when session.destroy produces an error', -> - - beforeEach -> - @req.session.destroy = sinon.stub().callsArgWith(0, new Error('woops')) - - it 'should call next with an error', (done) -> - @next = (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - @UserController.tryDeleteUser @req, @res, @next - - describe "unsubscribe", -> - - it "should send the user to unsubscribe", (done)-> - @res.send = (code)=> - @NewsLetterManager.unsubscribe.calledWith(@user).should.equal true - done() - @UserController.unsubscribe @req, @res - - describe "updateUserSettings", -> - beforeEach -> - @newEmail = "hello@world.com" - @req.externalAuthenticationSystemUsed = sinon.stub().returns(false) - - it "should call save", (done)-> - @req.body = {} - @res.sendStatus = (code)=> - @user.save.called.should.equal true - done() - @UserController.updateUserSettings @req, @res - - it "should set the first name", (done)-> - @req.body = - first_name: "bobby " - @res.sendStatus = (code)=> - @user.first_name.should.equal "bobby" - done() - @UserController.updateUserSettings @req, @res - - it "should set the role", (done)-> - @req.body = - role: "student" - @res.sendStatus = (code)=> - @user.role.should.equal "student" - done() - @UserController.updateUserSettings @req, @res - - it "should set the institution", (done)-> - @req.body = - institution: "MIT" - @res.sendStatus = (code)=> - @user.institution.should.equal "MIT" - done() - @UserController.updateUserSettings @req, @res - - it "should set some props on ace", (done)-> - @req.body = - editorTheme: "something" - @res.sendStatus = (code)=> - @user.ace.theme.should.equal "something" - done() - @UserController.updateUserSettings @req, @res - - it "should set the overall theme", (done)-> - @req.body = - overallTheme: "green-ish" - @res.sendStatus = (code)=> - @user.ace.overallTheme.should.equal "green-ish" - done() - @UserController.updateUserSettings @req, @res - - it "should send an error if the email is 0 len", (done)-> - @req.body.email = "" - @res.sendStatus = (code)-> - code.should.equal 400 - done() - @UserController.updateUserSettings @req, @res - - it "should send an error if the email does not contain an @", (done)-> - @req.body.email = "bob at something dot com" - @res.sendStatus = (code)-> - code.should.equal 400 - done() - @UserController.updateUserSettings @req, @res - - it "should call the user updater with the new email and user _id", (done)-> - @req.body.email = @newEmail.toUpperCase() - @UserUpdater.changeEmailAddress.callsArgWith(2) - @res.sendStatus = (code)=> - code.should.equal 200 - @UserUpdater.changeEmailAddress.calledWith(@user_id, @newEmail).should.equal true - done() - @UserController.updateUserSettings @req, @res - - it "should update the email on the session", (done)-> - @req.body.email = @newEmail.toUpperCase() - @UserUpdater.changeEmailAddress.callsArgWith(2) - callcount = 0 - @User.findById = (id, cb)=> - if ++callcount == 2 - @user.email = @newEmail - cb(null, @user) - @res.sendStatus = (code)=> - code.should.equal 200 - @AuthenticationController.setInSessionUser.calledWith( - @req, {email: @newEmail, first_name: undefined, last_name: undefined} - ).should.equal true - done() - @UserController.updateUserSettings @req, @res - - it "should call populateTeamInvites", (done)-> - @req.body.email = @newEmail.toUpperCase() - @UserUpdater.changeEmailAddress.callsArgWith(2) - @res.sendStatus = (code)=> - code.should.equal 200 - @UserHandler.populateTeamInvites.calledWith(@user).should.equal true - done() - @UserController.updateUserSettings @req, @res - - describe 'when using an external auth source', -> - - beforeEach -> - @UserUpdater.changeEmailAddress.callsArgWith(2) - @newEmail = 'someone23@example.com' - @req.externalAuthenticationSystemUsed = sinon.stub().returns(true) - - it 'should not set a new email', (done) -> - @req.body.email = @newEmail - @res.sendStatus = (code)=> - code.should.equal 200 - @UserUpdater.changeEmailAddress.calledWith(@user_id, @newEmail).should.equal false - done() - @UserController.updateUserSettings @req, @res - - describe "logout", -> - - it "should destroy the session", (done)-> - - @req.session.destroy = sinon.stub().callsArgWith(0) - @res.redirect = (url)=> - url.should.equal "/login" - @req.session.destroy.called.should.equal true - done() - - @UserController.logout @req, @res - - it 'should clear sudo-mode', (done) -> - @req.session.destroy = sinon.stub().callsArgWith(0) - @SudoModeHandler.clearSudoMode = sinon.stub() - @res.redirect = (url)=> - url.should.equal "/login" - @SudoModeHandler.clearSudoMode.callCount.should.equal 1 - @SudoModeHandler.clearSudoMode.calledWith(@user._id).should.equal true - done() - - @UserController.logout @req, @res - - - describe "register", -> - beforeEach -> - @UserRegistrationHandler.registerNewUserAndSendActivationEmail = sinon.stub().callsArgWith(1, null, @user, @url = "mock/url") - @req.body.email = @user.email = @email = "email@example.com" - @UserController.register @req, @res - - it "should register the user and send them an email", -> - @UserRegistrationHandler.registerNewUserAndSendActivationEmail - .calledWith(@email) - .should.equal true - - it "should return the user and activation url", -> - @res.json - .calledWith({ - email: @email, - setNewPasswordUrl: @url - }) - .should.equal true - - describe 'clearSessions', -> - - it 'should call revokeAllUserSessions', (done) -> - @UserController.clearSessions @req, @res - @UserSessionsManager.revokeAllUserSessions.callCount.should.equal 1 - done() - - it 'send a 201 response', (done) -> - @res.sendStatus = (status) => - status.should.equal 201 - done() - @UserController.clearSessions @req, @res - - describe 'when revokeAllUserSessions produces an error', -> - - it 'should call next with an error', (done) -> - @UserSessionsManager.revokeAllUserSessions.callsArgWith(2, new Error('woops')) - next = (err) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - @UserController.clearSessions @req, @res, next - - describe "changePassword", -> - - it "should check the old password is the current one at the moment", (done)-> - @AuthenticationManager.authenticate.callsArgWith(2) - @req.body = - currentPassword: "oldpasshere" - @res.send = => - @AuthenticationManager.authenticate.calledWith(_id:@user._id, "oldpasshere").should.equal true - @AuthenticationManager.setUserPassword.called.should.equal false - done() - @UserController.changePassword @req, @res - - it "it should not set the new password if they do not match", (done)-> - @AuthenticationManager.authenticate.callsArgWith(2, null, {}) - @req.body = - newPassword1: "1" - newPassword2: "2" - @res.send = => - @AuthenticationManager.setUserPassword.called.should.equal false - done() - @UserController.changePassword @req, @res - - it "should set the new password if they do match", (done)-> - @AuthenticationManager.authenticate.callsArgWith(2, null, @user) - @AuthenticationManager.setUserPassword.callsArgWith(2) - @req.body = - newPassword1: "newpass" - newPassword2: "newpass" - @res.send = => - @AuthenticationManager.setUserPassword.calledWith(@user._id, "newpass").should.equal true - done() - @UserController.changePassword @req, @res - - it "it should not set the new password if it is invalid", (done)-> - @AuthenticationManager.validatePassword = sinon.stub().returns { message: 'password contains invalid characters' } - @AuthenticationManager.authenticate.callsArgWith(2, null, {}) - @req.body = - newPassword1: "correct horse battery staple" - newPassword2: "correct horse battery staple" - @res.send = => - @AuthenticationManager.setUserPassword.called.should.equal false - done() - @UserController.changePassword @req, @res diff --git a/services/web/test/unit/coffee/User/UserCreatorTests.coffee b/services/web/test/unit/coffee/User/UserCreatorTests.coffee deleted file mode 100644 index aac4e96dcc..0000000000 --- a/services/web/test/unit/coffee/User/UserCreatorTests.coffee +++ /dev/null @@ -1,99 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -assert = require("assert") -should = chai.should() -modulePath = "../../../../app/js/Features/User/UserCreator.js" -SandboxedModule = require('sandboxed-module') - -describe "UserCreator", -> - - beforeEach -> - self = @ - @user = {_id:"12390i", ace: {}} - @user.save = sinon.stub().callsArgWith(0) - @UserModel = class Project - constructor: -> - return self.user - - @UserGetter = - getUserByMainEmail: sinon.stub() - @addAffiliation = sinon.stub().yields() - @UserCreator = SandboxedModule.require modulePath, requires: - "../../models/User": User:@UserModel - "logger-sharelatex":{ log: sinon.stub(), err: sinon.stub() } - 'metrics-sharelatex': {timeAsyncMethod: ()->} - "../Institutions/InstitutionsAPI": addAffiliation: @addAffiliation - - @email = "bob.oswald@gmail.com" - - describe "createNewUser", -> - - it "should take the opts and put them in the model", (done)-> - opts = - email:@email - holdingAccount:true - @UserCreator.createNewUser opts, (err, user)=> - assert.equal user.email, @email - assert.equal user.holdingAccount, true - assert.equal user.first_name, "bob.oswald" - done() - - it "should use the start of the email if the first name is empty string", (done)-> - opts = - email:@email - holdingAccount:true - first_name:"" - @UserCreator.createNewUser opts, (err, user)=> - assert.equal user.email, @email - assert.equal user.holdingAccount, true - assert.equal user.first_name, "bob.oswald" - done() - - - it "should use the first name if passed", (done)-> - opts = - email:@email - holdingAccount:true - first_name:"fiiirstname" - @UserCreator.createNewUser opts, (err, user)=> - assert.equal user.email, @email - assert.equal user.holdingAccount, true - assert.equal user.first_name, "fiiirstname" - done() - - it "should use the last name if passed", (done)-> - opts = - email:@email - holdingAccount:true - last_name:"lastNammmmeee" - @UserCreator.createNewUser opts, (err, user)=> - assert.equal user.email, @email - assert.equal user.holdingAccount, true - assert.equal user.last_name, "lastNammmmeee" - done() - - it "should set emails attribute", (done)-> - @UserCreator.createNewUser email: @email, (err, user)=> - user.email.should.equal @email - user.emails.length.should.equal 1 - user.emails[0].email.should.equal @email - user.emails[0].createdAt.should.be.a 'date' - user.emails[0].reversedHostname.should.equal "moc.liamg" - done() - - it "should add affiliation in background", (done)-> - @UserCreator.createNewUser email: @email, (err, user) => - # addaffiliation should not be called before the callback but only after - # a tick of the event loop - sinon.assert.notCalled(@addAffiliation) - process.nextTick () => - sinon.assert.calledWith(@addAffiliation, user._id, user.email) - done() - - it "should not add affiliation if skipping", (done)-> - attributes = email: @email - options = skip_affiliation: true - @UserCreator.createNewUser attributes, options, (err, user) => - process.nextTick () => - sinon.assert.notCalled(@addAffiliation) - done() diff --git a/services/web/test/unit/coffee/User/UserDeleterTests.coffee b/services/web/test/unit/coffee/User/UserDeleterTests.coffee deleted file mode 100644 index 3d1f774e6d..0000000000 --- a/services/web/test/unit/coffee/User/UserDeleterTests.coffee +++ /dev/null @@ -1,204 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/User/UserDeleter.js" -SandboxedModule = require('sandboxed-module') -Errors = require('../../../../app/js/Features/Errors/Errors') - -describe "UserDeleter", -> - - beforeEach -> - @user = - _id: "12390i" - email: "bob@bob.com" - remove: sinon.stub().callsArgWith(0) - - @User = - findById : sinon.stub().callsArgWith(1, null, @user) - - @NewsletterManager = - unsubscribe: sinon.stub().callsArgWith(1) - - @ProjectDeleter = - deleteUsersProjects: sinon.stub().callsArgWith(1) - - @SubscriptionHandler = - cancelSubscription: sinon.stub().callsArgWith(1) - - @SubscriptionUpdater = - removeUserFromAllGroups: sinon.stub().callsArgWith(1) - - @SubscriptionLocator = - getUsersSubscription: sinon.stub().yields(null, null) - - @UserMembershipsHandler = - removeUserFromAllEntities: sinon.stub().callsArgWith(1) - - @deleteAffiliations = sinon.stub().callsArgWith(1) - - @mongojs = - db: - deletedUsers: - insert: sinon.stub().callsArg(1) - usersDeletedByMigration: - insert: sinon.stub().callsArg(1) - - @UserDeleter = SandboxedModule.require modulePath, requires: - "../../models/User": User: @User - "../Newsletter/NewsletterManager": @NewsletterManager - "../Subscription/SubscriptionHandler": @SubscriptionHandler - "../Subscription/SubscriptionUpdater": @SubscriptionUpdater - "../Subscription/SubscriptionLocator": @SubscriptionLocator - "../UserMembership/UserMembershipsHandler": @UserMembershipsHandler - "../Project/ProjectDeleter": @ProjectDeleter - "../Institutions/InstitutionsAPI": - deleteAffiliations: @deleteAffiliations - "../../infrastructure/mongojs": @mongojs - "logger-sharelatex": @logger = { log: sinon.stub(), err: sinon.stub() } - "../Errors/Errors": Errors - - describe "softDeleteUserForMigration", -> - beforeEach -> - @UserDeleter._ensureCanDeleteUser = sinon.stub().yields(null) - - it "should delete the user in mongo", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - @User.findById.calledWith(@user._id).should.equal true - @user.remove.called.should.equal true - done() - - it "should add the user to the deletedUsers collection", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - sinon.assert.calledWith(@mongojs.db.usersDeletedByMigration.insert, @user) - done() - - it "should set the deletedAt field on the user", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - @user.deletedAt.should.exist - done() - - it "should unsubscribe the user from the news letter", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - @NewsletterManager.unsubscribe.calledWith(@user).should.equal true - done() - - it "should unsubscribe the user", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - @SubscriptionHandler.cancelSubscription.calledWith(@user).should.equal true - done() - - it "should delete user affiliations", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - @deleteAffiliations.calledWith(@user._id).should.equal true - done() - - it "should delete all the projects of a user", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - @ProjectDeleter.deleteUsersProjects.calledWith(@user._id).should.equal true - done() - - it "should remove user memberships", (done)-> - @UserDeleter.softDeleteUserForMigration @user._id, (err)=> - @UserMembershipsHandler.removeUserFromAllEntities.calledWith(@user._id).should.equal true - done() - - it "ensures user can be deleted first", (done)-> - @UserDeleter._ensureCanDeleteUser.yields( - new Errors.SubscriptionAdminDeletionError() - ) - @UserDeleter.softDeleteUserForMigration @user._id, (error) => - sinon.assert.calledWith(@UserDeleter._ensureCanDeleteUser, @user) - sinon.assert.notCalled(@user.remove) - expect(error).to.be.instanceof Errors.SubscriptionAdminDeletionError - done() - - describe "deleteUser", -> - beforeEach -> - @UserDeleter._ensureCanDeleteUser = sinon.stub().yields(null) - - it "should delete the user in mongo", (done)-> - @UserDeleter.deleteUser @user._id, (err)=> - @User.findById.calledWith(@user._id).should.equal true - @user.remove.called.should.equal true - done() - - it "should unsubscribe the user from the news letter", (done)-> - @UserDeleter.deleteUser @user._id, (err)=> - @NewsletterManager.unsubscribe.calledWith(@user).should.equal true - done() - - it "should delete all the projects of a user", (done)-> - @UserDeleter.deleteUser @user._id, (err)=> - @ProjectDeleter.deleteUsersProjects.calledWith(@user._id).should.equal true - done() - - it "should unsubscribe the user", (done)-> - @UserDeleter.deleteUser @user._id, (err)=> - @SubscriptionHandler.cancelSubscription.calledWith(@user).should.equal true - done() - - it "should delete user affiliations", (done)-> - @UserDeleter.deleteUser @user._id, (err)=> - @deleteAffiliations.calledWith(@user._id).should.equal true - done() - - it "should remove user from group subscriptions", (done)-> - @UserDeleter.deleteUser @user._id, (err)=> - @SubscriptionUpdater.removeUserFromAllGroups.calledWith(@user._id).should.equal true - done() - - it "should remove user memberships", (done)-> - @UserDeleter.deleteUser @user._id, (err)=> - @UserMembershipsHandler.removeUserFromAllEntities.calledWith(@user._id).should.equal true - done() - - it "ensures user can be deleted first", (done)-> - @UserDeleter._ensureCanDeleteUser.yields( - new Errors.SubscriptionAdminDeletionError() - ) - @UserDeleter.deleteUser @user._id, (error) => - sinon.assert.calledWith(@UserDeleter._ensureCanDeleteUser, @user) - sinon.assert.notCalled(@user.remove) - expect(error).to.be.instanceof Errors.SubscriptionAdminDeletionError - done() - - describe "when unsubscribing from mailchimp fails", -> - beforeEach -> - @NewsletterManager.unsubscribe = sinon.stub().callsArgWith(1, new Error("something went wrong")) - - it "should not return an error", (done) -> - @UserDeleter.deleteUser @user._id, (err)=> - @NewsletterManager.unsubscribe.calledWith(@user).should.equal true - should.not.exist(err) - done() - - it "should delete the user", (done) -> - @UserDeleter.deleteUser @user._id, (err)=> - @NewsletterManager.unsubscribe.calledWith(@user).should.equal true - @user.remove.called.should.equal true - done() - - it "should log an error", (done) -> - @UserDeleter.deleteUser @user._id, (err)=> - sinon.assert.called(@logger.err) - done() - - describe '_ensureCanDeleteUser', -> - it 'should not return error when user can be deleted', (done) -> - @SubscriptionLocator.getUsersSubscription.yields(null, null) - @UserDeleter._ensureCanDeleteUser @user, (error) -> - expect(error).to.not.exist - done() - - it 'should return custom error when user is group admin', (done) -> - @SubscriptionLocator.getUsersSubscription.yields(null, { _id: '123abc' }) - @UserDeleter._ensureCanDeleteUser @user, (error) -> - expect(error).to.be.instanceof Errors.SubscriptionAdminDeletionError - done() - - it 'propagate errors', (done) -> - @SubscriptionLocator.getUsersSubscription.yields(new Error('Some error')) - @UserDeleter._ensureCanDeleteUser @user, (error) -> - expect(error).to.be.instanceof Error - done() diff --git a/services/web/test/unit/coffee/User/UserEmailsConfirmationHandlerTests.coffee b/services/web/test/unit/coffee/User/UserEmailsConfirmationHandlerTests.coffee deleted file mode 100644 index 79aba74fe7..0000000000 --- a/services/web/test/unit/coffee/User/UserEmailsConfirmationHandlerTests.coffee +++ /dev/null @@ -1,138 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/User/UserEmailsConfirmationHandler" -expect = require("chai").expect -Errors = require "../../../../app/js/Features/Errors/Errors" -EmailHelper = require "../../../../app/js/Features/Helpers/EmailHelper" - -describe "UserEmailsConfirmationHandler", -> - beforeEach -> - @UserEmailsConfirmationHandler = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = - siteUrl: "emails.example.com" - "logger-sharelatex": @logger = { log: sinon.stub() } - "../Security/OneTimeTokenHandler": @OneTimeTokenHandler = {} - "../Errors/Errors": Errors - "./UserUpdater": @UserUpdater = {} - "./UserGetter": @UserGetter = - getUser: sinon.stub().yields(null, @mockUser) - "../Email/EmailHandler": @EmailHandler = {} - "../Helpers/EmailHelper": EmailHelper - @mockUser = _id: "mock-user-id" - @user_id = @mockUser._id - @email = "mock@example.com" - @callback = sinon.stub() - - describe "sendConfirmationEmail", -> - beforeEach -> - @OneTimeTokenHandler.getNewToken = sinon.stub().yields(null, @token = "new-token") - @EmailHandler.sendEmail = sinon.stub().yields() - - describe 'successfully', -> - beforeEach -> - @UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, @email, @callback - - it "should generate a token for the user which references their id and email", -> - @OneTimeTokenHandler.getNewToken - .calledWith( - 'email_confirmation', - {@user_id, @email}, - { expiresIn: 365 * 24 * 60 * 60 } - ) - .should.equal true - - it 'should send an email to the user', -> - @EmailHandler.sendEmail - .calledWith('confirmEmail', { - to: @email, - confirmEmailUrl: 'emails.example.com/user/emails/confirm?token=new-token' - sendingUser_id: @user_id - }) - .should.equal true - - it 'should call the callback', -> - @callback.called.should.equal true - - describe 'with invalid email', -> - beforeEach -> - @UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, '!"£$%^&*()', @callback - - it 'should return an error', -> - @callback.calledWith(sinon.match.instanceOf(Error)).should.equal true - - describe 'a custom template', -> - beforeEach -> - @UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, @email, 'myCustomTemplate', @callback - - it 'should send an email with the given template', -> - @EmailHandler.sendEmail - .calledWith('myCustomTemplate') - .should.equal true - - describe "confirmEmailFromToken", -> - beforeEach -> - @OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields( - null, - {@user_id, @email} - ) - @UserUpdater.confirmEmail = sinon.stub().yields() - - describe "successfully", -> - beforeEach -> - @UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback - - it "should call getValueFromTokenAndExpire", -> - @OneTimeTokenHandler.getValueFromTokenAndExpire - .calledWith('email_confirmation', @token) - .should.equal true - - it "should confirm the email of the user_id", -> - @UserUpdater.confirmEmail - .calledWith(@user_id, @email) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe 'with an expired token', -> - beforeEach -> - @OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(null, null) - @UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback - - it "should call the callback with a NotFoundError", -> - @callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true - - describe 'with no user_id in the token', -> - beforeEach -> - @OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields( - null, - {@email} - ) - @UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback - - it "should call the callback with a NotFoundError", -> - @callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true - - describe 'with no email in the token', -> - beforeEach -> - @OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields( - null, - {@user_id} - ) - @UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback - - it "should call the callback with a NotFoundError", -> - @callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true - - - describe 'with no user found', -> - beforeEach -> - @UserGetter.getUser.yields(null, null) - @UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback - - it "should call the callback with a NotFoundError", -> - @callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true - diff --git a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee b/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee deleted file mode 100644 index 800a44336f..0000000000 --- a/services/web/test/unit/coffee/User/UserEmailsControllerTests.coffee +++ /dev/null @@ -1,210 +0,0 @@ -sinon = require('sinon') -assertCalledWith = sinon.assert.calledWith -assertNotCalled = sinon.assert.notCalled -chai = require('chai') -should = chai.should() -assert = chai.assert -modulePath = "../../../../app/js/Features/User/UserEmailsController.js" -SandboxedModule = require('sandboxed-module') -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -Errors = require("../../../../app/js/Features/Errors/Errors") - -describe "UserEmailsController", -> - beforeEach -> - @req = new MockRequest() - @user = - _id: 'mock-user-id' - - @UserGetter = - getUserFullEmails: sinon.stub() - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user._id) - @UserUpdater = - addEmailAddress: sinon.stub() - removeEmailAddress: sinon.stub() - setDefaultEmailAddress: sinon.stub() - updateV1AndSetDefaultEmailAddress: sinon.stub() - @EmailHelper = - parseEmail: sinon.stub() - @endorseAffiliation = sinon.stub().yields() - @UserEmailsController = SandboxedModule.require modulePath, requires: - "../Authentication/AuthenticationController": @AuthenticationController - "./UserGetter": @UserGetter - "./UserUpdater": @UserUpdater - "../Helpers/EmailHelper": @EmailHelper - "./UserEmailsConfirmationHandler": @UserEmailsConfirmationHandler = {} - "../Institutions/InstitutionsAPI": endorseAffiliation: @endorseAffiliation - "../Errors/Errors": Errors - "logger-sharelatex": - log: -> console.log(arguments) - err: -> - - describe 'List', -> - beforeEach -> - - it 'lists emails', (done) -> - fullEmails = [{some: 'data'}] - @UserGetter.getUserFullEmails.callsArgWith 1, null, fullEmails - - @UserEmailsController.list @req, - json: (response) => - assert.deepEqual response, fullEmails - assertCalledWith @UserGetter.getUserFullEmails, @user._id - done() - - describe 'Add', -> - beforeEach -> - @newEmail = 'new_email@baz.com' - @req.body = - email: @newEmail - university: { name: 'University Name' } - department: 'Department' - role: 'Role' - @EmailHelper.parseEmail.returns @newEmail - @UserEmailsConfirmationHandler.sendConfirmationEmail = sinon.stub().yields() - @UserUpdater.addEmailAddress.callsArgWith 3, null - - it 'adds new email', (done) -> - @UserEmailsController.add @req, - sendStatus: (code) => - code.should.equal 204 - assertCalledWith @EmailHelper.parseEmail, @newEmail - assertCalledWith @UserUpdater.addEmailAddress, @user._id, @newEmail - - affiliationOptions = @UserUpdater.addEmailAddress.lastCall.args[2] - Object.keys(affiliationOptions).length.should.equal 3 - affiliationOptions.university.should.equal @req.body.university - affiliationOptions.department.should.equal @req.body.department - affiliationOptions.role.should.equal @req.body.role - - done() - - it 'sends an email confirmation', (done) -> - @UserEmailsController.add @req, - sendStatus: (code) => - code.should.equal 204 - assertCalledWith @UserEmailsConfirmationHandler.sendConfirmationEmail, @user._id, @newEmail - done() - - it 'handles email parse error', (done) -> - @EmailHelper.parseEmail.returns null - @UserEmailsController.add @req, - sendStatus: (code) => - code.should.equal 422 - assertNotCalled @UserUpdater.addEmailAddress - done() - - describe 'remove', -> - beforeEach -> - @email = 'email_to_remove@bar.com' - @req.body.email = @email - @EmailHelper.parseEmail.returns @email - - it 'removes email', (done) -> - @UserUpdater.removeEmailAddress.callsArgWith 2, null - - @UserEmailsController.remove @req, - sendStatus: (code) => - code.should.equal 200 - assertCalledWith @EmailHelper.parseEmail, @email - assertCalledWith @UserUpdater.removeEmailAddress, @user._id, @email - done() - - it 'handles email parse error', (done) -> - @EmailHelper.parseEmail.returns null - - @UserEmailsController.remove @req, - sendStatus: (code) => - code.should.equal 422 - assertNotCalled @UserUpdater.removeEmailAddress - done() - - describe 'setDefault', -> - beforeEach -> - @email = "email_to_set_default@bar.com" - @req.body.email = @email - @EmailHelper.parseEmail.returns @email - - it 'sets default email', (done) -> - @UserUpdater.updateV1AndSetDefaultEmailAddress.callsArgWith 2, null - - @UserEmailsController.setDefault @req, - sendStatus: (code) => - code.should.equal 200 - assertCalledWith @EmailHelper.parseEmail, @email - assertCalledWith @UserUpdater.updateV1AndSetDefaultEmailAddress, @user._id, @email - done() - - it 'handles email parse error', (done) -> - @EmailHelper.parseEmail.returns null - - @UserEmailsController.setDefault @req, - sendStatus: (code) => - code.should.equal 422 - assertNotCalled @UserUpdater.setDefaultEmailAddress - done() - - describe 'endorse', -> - beforeEach -> - @email = 'email_to_endorse@bar.com' - @req.body.email = @email - @EmailHelper.parseEmail.returns @email - - it 'endorses affiliation', (done) -> - @req.body.role = 'Role' - @req.body.department = 'Department' - - @UserEmailsController.endorse @req, - sendStatus: (code) => - code.should.equal 204 - assertCalledWith @endorseAffiliation, @user._id, @email, 'Role', 'Department' - done() - - describe 'confirm', -> - beforeEach -> - @UserEmailsConfirmationHandler.confirmEmailFromToken = sinon.stub().yields() - @res = - sendStatus: sinon.stub() - json: sinon.stub() - @res.status = sinon.stub().returns(@res) - @next = sinon.stub() - @token = 'mock-token' - @req.body = token: @token - - describe 'successfully', -> - beforeEach -> - @UserEmailsController.confirm @req, @res, @next - - it 'should confirm the email from the token', -> - @UserEmailsConfirmationHandler.confirmEmailFromToken - .calledWith(@token) - .should.equal true - - it 'should return a 200 status', -> - @res.sendStatus.calledWith(200).should.equal true - - describe 'without a token', -> - beforeEach -> - @req.body.token = null - @UserEmailsController.confirm @req, @res, @next - - it 'should return a 422 status', -> - @res.sendStatus.calledWith(422).should.equal true - - describe 'when confirming fails', -> - beforeEach -> - @UserEmailsConfirmationHandler.confirmEmailFromToken = sinon.stub().yields( - new Errors.NotFoundError('not found') - ) - @UserEmailsController.confirm @req, @res, @next - - it 'should return a 404 error code with a message', -> - @res.status.calledWith(404).should.equal true - @res.json.calledWith({ - message: 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.' - }).should.equal true - - - - diff --git a/services/web/test/unit/coffee/User/UserGetterTests.coffee b/services/web/test/unit/coffee/User/UserGetterTests.coffee deleted file mode 100644 index 0691ed0cdb..0000000000 --- a/services/web/test/unit/coffee/User/UserGetterTests.coffee +++ /dev/null @@ -1,196 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/User/UserGetter" -expect = require("chai").expect -Errors = require "../../../../app/js/Features/Errors/Errors" - -describe "UserGetter", -> - - beforeEach -> - @fakeUser = - _id: '12390i' - email: 'email2@foo.bar' - emails: [ - { email: 'email1@foo.bar', reversedHostname: 'rab.oof' } - { email: 'email2@foo.bar', reversedHostname: 'rab.oof' } - ] - @findOne = sinon.stub().callsArgWith(2, null, @fakeUser) - @find = sinon.stub().callsArgWith(2, null, [ @fakeUser ]) - @Mongo = - db: users: - findOne: @findOne - find: @find - ObjectId: (id) -> return id - settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } - @getUserAffiliations = sinon.stub().callsArgWith(1, null, []) - - @UserGetter = SandboxedModule.require modulePath, requires: - "logger-sharelatex": log:-> - "../../infrastructure/mongojs": @Mongo - "metrics-sharelatex": timeAsyncMethod: sinon.stub() - 'settings-sharelatex': settings - '../Institutions/InstitutionsAPI': - getUserAffiliations: @getUserAffiliations - "../Errors/Errors": Errors - - describe "getUser", -> - it "should get user", (done)-> - query = _id: 'foo' - projection = email: 1 - @UserGetter.getUser query, projection, (error, user) => - @findOne.called.should.equal true - @findOne.calledWith(query, projection).should.equal true - user.should.deep.equal @fakeUser - done() - - it "should not allow email in query", (done)-> - @UserGetter.getUser email: 'foo@bar.com', {}, (error, user) => - error.should.exist - done() - - it "should not allow null query", (done)-> - @UserGetter.getUser null, {}, (error, user) => - error.should.exist - done() - - describe "getUserFullEmails", -> - it "should get user", (done)-> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @fakeUser) - projection = email: 1, emails: 1 - @UserGetter.getUserFullEmails @fakeUser._id, (error, fullEmails) => - @UserGetter.getUser.called.should.equal true - @UserGetter.getUser.calledWith(@fakeUser._id, projection).should.equal true - done() - - it "should fetch emails data", (done)-> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @fakeUser) - @UserGetter.getUserFullEmails @fakeUser._id, (error, fullEmails) => - assert.deepEqual fullEmails, [ - { email: 'email1@foo.bar', reversedHostname: 'rab.oof', default: false } - { email: 'email2@foo.bar', reversedHostname: 'rab.oof', default: true } - ] - done() - - it "should merge affiliation data", (done)-> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @fakeUser) - affiliationsData = [ - { - email: 'email1@foo.bar' - role: 'Prof' - department: 'Maths' - inferred: false - institution: { name: 'University Name', isUniversity: true } - } - ] - @getUserAffiliations.callsArgWith(1, null, affiliationsData) - @UserGetter.getUserFullEmails @fakeUser._id, (error, fullEmails) => - assert.deepEqual fullEmails, [ - { - email: 'email1@foo.bar', - reversedHostname: 'rab.oof' - default: false - affiliation: - institution: affiliationsData[0].institution - inferred: affiliationsData[0].inferred - department: affiliationsData[0].department - role: affiliationsData[0].role - } - { email: 'email2@foo.bar', reversedHostname: 'rab.oof', default: true } - ] - done() - - it "should get user when it has no emails field", (done)-> - @fakeUser = - _id: '12390i' - email: 'email2@foo.bar' - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @fakeUser) - projection = email: 1, emails: 1 - @UserGetter.getUserFullEmails @fakeUser._id, (error, fullEmails) => - @UserGetter.getUser.called.should.equal true - @UserGetter.getUser.calledWith(@fakeUser._id, projection).should.equal true - assert.deepEqual fullEmails, [] - done() - - describe "getUserbyMainEmail", -> - it "query user by main email", (done)-> - email = 'hello@world.com' - projection = emails: 1 - @UserGetter.getUserByMainEmail email, projection, (error, user) => - @findOne.called.should.equal true - @findOne.calledWith(email: email, projection).should.equal true - done() - - it "return user if found", (done)-> - email = 'hello@world.com' - @UserGetter.getUserByMainEmail email, (error, user) => - user.should.deep.equal @fakeUser - done() - - it "trim email", (done)-> - email = 'hello@world.com' - @UserGetter.getUserByMainEmail " #{email} ", (error, user) => - @findOne.called.should.equal true - @findOne.calledWith(email: email).should.equal true - done() - - describe "getUserByAnyEmail", -> - it "query user for any email", (done)-> - email = 'hello@world.com' - expectedQuery = - emails: { $exists: true } - 'emails.email': email - projection = emails: 1 - @UserGetter.getUserByAnyEmail " #{email} ", projection, (error, user) => - @findOne.calledWith(expectedQuery, projection).should.equal true - user.should.deep.equal @fakeUser - done() - - it "query contains $exists:true so partial index is used", (done)-> - expectedQuery = - emails: { $exists: true } - 'emails.email': '' - @UserGetter.getUserByAnyEmail '', {}, (error, user) => - @findOne.calledWith(expectedQuery, {}).should.equal true - done() - - it "checks main email as well", (done)-> - @findOne.callsArgWith(2, null, null) - email = 'hello@world.com' - projection = emails: 1 - @UserGetter.getUserByAnyEmail " #{email} ", projection, (error, user) => - @findOne.calledTwice.should.equal true - @findOne.calledWith(email: email, projection).should.equal true - done() - - describe "getUsersByHostname", -> - it "should find user by hostname", (done)-> - hostname = "bar.foo" - expectedQuery = - emails: {$exists: true }, - 'emails.reversedHostname': hostname.split('').reverse().join('') - projection = emails: 1 - @UserGetter.getUsersByHostname hostname, projection, (error, users) => - @find.calledOnce.should.equal true - @find.calledWith(expectedQuery, projection).should.equal true - done() - - describe 'ensureUniqueEmailAddress', -> - beforeEach -> - @UserGetter.getUserByAnyEmail = sinon.stub() - - it 'should return error if existing user is found', (done)-> - @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @fakeUser) - @UserGetter.ensureUniqueEmailAddress @newEmail, (err)=> - should.exist(err) - expect(err).to.be.an.instanceof(Errors.EmailExistsError) - err.message.should.equal 'alread_exists' - done() - - it 'should return null if no user is found', (done)-> - @UserGetter.getUserByAnyEmail.callsArgWith(1) - @UserGetter.ensureUniqueEmailAddress @newEmail, (err)=> - should.not.exist(err) - done() diff --git a/services/web/test/unit/coffee/User/UserHandlerTests.coffee b/services/web/test/unit/coffee/User/UserHandlerTests.coffee deleted file mode 100644 index 0a6207ed19..0000000000 --- a/services/web/test/unit/coffee/User/UserHandlerTests.coffee +++ /dev/null @@ -1,28 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -modulePath = "../../../../app/js/Features/User/UserHandler.js" -SandboxedModule = require('sandboxed-module') - -describe "UserHandler", -> - - beforeEach -> - @user = - _id:"12390i" - email: "bob@bob.com" - remove: sinon.stub().callsArgWith(0) - - @TeamInvitesHandler = - createTeamInvitesForLegacyInvitedEmail: sinon.stub().yields() - - @UserHandler = SandboxedModule.require modulePath, requires: - "../Subscription/TeamInvitesHandler": @TeamInvitesHandler - - describe "populateTeamInvites", -> - beforeEach (done)-> - @UserHandler.populateTeamInvites @user, done - - it "notifies the user about legacy team invites", -> - @TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail - .calledWith(@user.email).should.eq true - diff --git a/services/web/test/unit/coffee/User/UserInfoControllerTests.coffee b/services/web/test/unit/coffee/User/UserInfoControllerTests.coffee deleted file mode 100644 index 7f1c0917d6..0000000000 --- a/services/web/test/unit/coffee/User/UserInfoControllerTests.coffee +++ /dev/null @@ -1,162 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -assert = require("chai").assert -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/User/UserInfoController.js" -SandboxedModule = require('sandboxed-module') -events = require "events" -MockResponse = require "../helpers/MockResponse" -MockRequest = require "../helpers/MockRequest" -ObjectId = require("mongojs").ObjectId - -describe "UserInfoController", -> - beforeEach -> - @UserDeleter = - deleteUser: sinon.stub().callsArgWith(1) - @UserUpdater = - updatePersonalInfo: sinon.stub() - @sanitizer = escape:(v)->v - sinon.spy @sanitizer, "escape" - @UserGetter = {} - - - @UserInfoController = SandboxedModule.require modulePath, requires: - "./UserGetter": @UserGetter - "./UserUpdater": @UserUpdater - "./UserDeleter": @UserDeleter - "logger-sharelatex": log:-> - "sanitizer":@sanitizer - '../Authentication/AuthenticationController': @AuthenticationController = {getLoggedInUserId: sinon.stub()} - - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - - describe "getLoggedInUsersPersonalInfo", -> - beforeEach -> - @user = - _id: ObjectId() - @req.user = @user - @req.session.user = @user - @UserInfoController.sendFormattedPersonalInfo = sinon.stub() - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) - @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user._id) - @UserInfoController.getLoggedInUsersPersonalInfo(@req, @res, @next) - - it "should call sendFormattedPersonalInfo", -> - @UserInfoController.sendFormattedPersonalInfo - .calledWith(@user, @res, @next) - .should.equal true - - describe "getPersonalInfo", -> - describe "when the user exists with sharelatex id", -> - beforeEach -> - @user_id = ObjectId().toString() - @user = - _id: ObjectId(@user_id) - @req.params = user_id: @user_id - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) - @UserInfoController.sendFormattedPersonalInfo = sinon.stub() - @UserInfoController.getPersonalInfo(@req, @res, @next) - - it "should look up the user in the database", -> - @UserGetter.getUser - .calledWith( - { _id: ObjectId(@user_id) }, - { _id: true, first_name: true, last_name: true, email: true } - ).should.equal true - - it "should send the formatted details back to the client", -> - @UserInfoController.sendFormattedPersonalInfo - .calledWith(@user, @res, @next) - .should.equal true - - describe "when the user exists with overleaf id", -> - beforeEach -> - @user_id = 12345 - @user = - _id: ObjectId() - overleaf: - id: @user_id - @req.params = user_id: @user_id.toString() - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) - @UserInfoController.sendFormattedPersonalInfo = sinon.stub() - @UserInfoController.getPersonalInfo(@req, @res, @next) - - it "should look up the user in the database", -> - @UserGetter.getUser - .calledWith( - { "overleaf.id": @user_id }, - { _id: true, first_name: true, last_name: true, email: true } - ).should.equal true - - it "should send the formatted details back to the client", -> - @UserInfoController.sendFormattedPersonalInfo - .calledWith(@user, @res, @next) - .should.equal true - - describe "when the user does not exist", -> - beforeEach -> - @user_id = ObjectId().toString() - @req.params = user_id: @user_id - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) - @UserInfoController.getPersonalInfo(@req, @res, @next) - - it "should return 404 to the client", -> - @res.statusCode.should.equal 404 - - describe "when the user id is invalid", -> - beforeEach -> - @user_id = "invalid" - @req.params = user_id: @user_id - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) - @UserInfoController.getPersonalInfo(@req, @res, @next) - - it "should return 400 to the client", -> - @res.statusCode.should.equal 400 - - describe "sendFormattedPersonalInfo", -> - beforeEach -> - @user = - _id: ObjectId() - first_name: "Douglas" - last_name: "Adams" - email: "doug@sharelatex.com" - @formattedInfo = - id: @user._id.toString() - first_name: @user.first_name - last_name: @user.last_name - email: @user.email - @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formattedInfo) - @UserInfoController.sendFormattedPersonalInfo @user, @res - - it "should format the user details for the response", -> - @UserInfoController.formatPersonalInfo - .calledWith(@user) - .should.equal true - - it "should send the formatted details back to the client", -> - @res.body.should.equal JSON.stringify(@formattedInfo) - - describe "formatPersonalInfo", -> - it "should return the correctly formatted data", -> - @user = - _id: ObjectId() - first_name: "Douglas" - last_name: "Adams" - email: "doug@sharelatex.com" - password: "should-not-get-included" - signUpDate: new Date() - role:"student" - institution:"sheffield" - expect(@UserInfoController.formatPersonalInfo(@user)).to.deep.equal { - id: @user._id.toString() - first_name: @user.first_name - last_name: @user.last_name - email: @user.email - signUpDate: @user.signUpDate - role: @user.role - institution: @user.institution - } - diff --git a/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee b/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee deleted file mode 100644 index 6412d7ecc1..0000000000 --- a/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee +++ /dev/null @@ -1,246 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/User/UserPagesController" -expect = require("chai").expect - -describe "UserPagesController", -> - - beforeEach -> - - @settings = { - apis: - v1: - url: 'some.host' - user: 'one' - pass: 'two' - } - @user = - _id: @user_id = "kwjewkl" - features:{} - email: "joe@example.com" - thirdPartyIdentifiers: [ - { - "providerId": "google", - "externalUserId": "testId" - } - ] - - @UserGetter = getUser: sinon.stub() - @UserSessionsManager = - getAllUserSessions: sinon.stub() - @dropboxStatus = {} - @DropboxHandler = - getUserRegistrationStatus : sinon.stub().callsArgWith(1, null, @dropboxStatus) - @ErrorController = - notFound: sinon.stub() - @AuthenticationController = - getLoggedInUserId: sinon.stub().returns(@user._id) - getSessionUser: sinon.stub().returns(@user) - _getRedirectFromSession: sinon.stub() - setRedirectInSession: sinon.stub() - @UserPagesController = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings - "logger-sharelatex": - log:-> - err:-> - "./UserGetter": @UserGetter - "./UserSessionsManager": @UserSessionsManager - "../Errors/ErrorController": @ErrorController - '../Dropbox/DropboxHandler': @DropboxHandler - '../Authentication/AuthenticationController': @AuthenticationController - 'request': @request = sinon.stub() - @req = - query:{} - session: - user:@user - @res = {} - - - describe "registerPage", -> - - it "should render the register page", (done)-> - @res.render = (page)=> - page.should.equal "user/register" - done() - @UserPagesController.registerPage @req, @res - - it "should set sharedProjectData", (done)-> - @req.query.project_name = "myProject" - @req.query.user_first_name = "user_first_name_here" - - @res.render = (page, opts)=> - opts.sharedProjectData.project_name.should.equal "myProject" - opts.sharedProjectData.user_first_name.should.equal "user_first_name_here" - done() - @UserPagesController.registerPage @req, @res - - it "should set newTemplateData", (done)-> - @req.session.templateData = - templateName : "templateName" - - @res.render = (page, opts)=> - opts.newTemplateData.templateName.should.equal "templateName" - done() - @UserPagesController.registerPage @req, @res - - it "should not set the newTemplateData if there is nothing in the session", (done)-> - @res.render = (page, opts)=> - assert.equal opts.newTemplateData.templateName, undefined - done() - @UserPagesController.registerPage @req, @res - - - describe "loginForm", -> - - it "should render the login page", (done)-> - @res.render = (page)=> - page.should.equal "user/login" - done() - @UserPagesController.loginPage @req, @res - - describe 'when an explicit redirect is set via query string', -> - - beforeEach -> - @AuthenticationController._getRedirectFromSession = sinon.stub().returns(null) - @AuthenticationController.setRedirectInSession = sinon.stub() - @req.query.redir = '/somewhere/in/particular' - - it 'should set a redirect', (done) -> - @res.render = (page) => - @AuthenticationController.setRedirectInSession.callCount.should.equal 1 - expect(@AuthenticationController.setRedirectInSession.lastCall.args[1]).to.equal @req.query.redir - done() - @UserPagesController.loginPage @req, @res - - describe 'sessionsPage', -> - - beforeEach -> - @UserSessionsManager.getAllUserSessions.callsArgWith(2, null, []) - - it 'should render user/sessions', (done) -> - @res.render = (page)-> - page.should.equal "user/sessions" - done() - @UserPagesController.sessionsPage @req, @res - - it 'should have called getAllUserSessions', (done) -> - @res.render = (page) => - @UserSessionsManager.getAllUserSessions.callCount.should.equal 1 - done() - @UserPagesController.sessionsPage @req, @res - - describe 'when getAllUserSessions produces an error', -> - - beforeEach -> - @UserSessionsManager.getAllUserSessions.callsArgWith(2, new Error('woops')) - - it 'should call next with an error', (done) -> - @next = (err) => - assert(err != null) - assert(err instanceof Error) - done() - @UserPagesController.sessionsPage @req, @res, @next - - describe "settingsPage", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, {has_password: true}) - @UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user) - - it "should render user/settings", (done)-> - @res.render = (page)-> - page.should.equal "user/settings" - done() - @UserPagesController.settingsPage @req, @res - - it "should send user", (done)-> - @res.render = (page, opts)=> - opts.user.should.equal @user - done() - @UserPagesController.settingsPage @req, @res - - it "should set 'shouldAllowEditingDetails' to true", (done)-> - @res.render = (page, opts)=> - opts.shouldAllowEditingDetails.should.equal true - done() - @UserPagesController.settingsPage @req, @res - - it "should restructure thirdPartyIdentifiers data for template use", (done)-> - expectedResult = { - google: "testId" - } - @res.render = (page, opts)=> - expect(opts.thirdPartyIds).to.include expectedResult - done() - @UserPagesController.settingsPage @req, @res - - describe 'when ldap.updateUserDetailsOnLogin is true', -> - - beforeEach -> - @settings.ldap = {updateUserDetailsOnLogin: true} - - afterEach -> - delete @settings.ldap - - it 'should set "shouldAllowEditingDetails" to false', (done) -> - @res.render = (page, opts)=> - opts.shouldAllowEditingDetails.should.equal false - done() - @UserPagesController.settingsPage @req, @res - - describe 'when saml.updateUserDetailsOnLogin is true', -> - - beforeEach -> - @settings.saml = {updateUserDetailsOnLogin: true} - - afterEach -> - delete @settings.saml - - it 'should set "shouldAllowEditingDetails" to false', (done) -> - @res.render = (page, opts)=> - opts.shouldAllowEditingDetails.should.equal false - done() - @UserPagesController.settingsPage @req, @res - - describe "activateAccountPage", -> - beforeEach -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) - @req.query.user_id = @user_id - @req.query.token = @token = "mock-token-123" - - it "should 404 without a user_id", (done) -> - delete @req.query.user_id - @ErrorController.notFound = () -> - done() - @UserPagesController.activateAccountPage @req, @res - - it "should 404 without a token", (done) -> - delete @req.query.token - @ErrorController.notFound = () -> - done() - @UserPagesController.activateAccountPage @req, @res - - it "should 404 without a valid user_id", (done) -> - @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) - @ErrorController.notFound = () -> - done() - @UserPagesController.activateAccountPage @req, @res - - it "should redirect activated users to login", (done) -> - @user.loginCount = 1 - @res.redirect = (url) => - @UserGetter.getUser.calledWith(@user_id).should.equal true - url.should.equal "/login?email=#{encodeURIComponent(@user.email)}" - done() - @UserPagesController.activateAccountPage @req, @res - - it "render the activation page if the user has not logged in before", (done) -> - @user.loginCount = 0 - @res.render = (page, opts) => - page.should.equal "user/activate" - opts.email.should.equal @user.email - opts.token.should.equal @token - done() - @UserPagesController.activateAccountPage @req, @res diff --git a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee b/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee deleted file mode 100644 index 7fcd2147a5..0000000000 --- a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee +++ /dev/null @@ -1,202 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -modulePath = path.join __dirname, '../../../../app/js/Features/User/UserRegistrationHandler' -sinon = require("sinon") -expect = require("chai").expect -EmailHelper = require '../../../../app/js/Features/Helpers/EmailHelper' - -describe "UserRegistrationHandler", -> - - beforeEach -> - @user = - _id: @user_id = "31j2lk21kjl" - @User = - update: sinon.stub().callsArgWith(2) - @UserGetter = - getUserByAnyEmail: sinon.stub() - @UserCreator = - createNewUser:sinon.stub().callsArgWith(1, null, @user) - @AuthenticationManager = - validateEmail: sinon.stub().returns(null) - validatePassword: sinon.stub().returns(null) - setUserPassword: sinon.stub().callsArgWith(2) - @NewsLetterManager = - subscribe: sinon.stub().callsArgWith(1) - @EmailHandler = - sendEmail:sinon.stub().callsArgWith(2) - @OneTimeTokenHandler = - getNewToken: sinon.stub() - @handler = SandboxedModule.require modulePath, requires: - "../../models/User": {User:@User} - "./UserGetter": @UserGetter - "./UserCreator": @UserCreator - "../Authentication/AuthenticationManager":@AuthenticationManager - "../Newsletter/NewsletterManager":@NewsLetterManager - "logger-sharelatex": @logger = { log: sinon.stub() } - "crypto": @crypto = {} - "../Email/EmailHandler": @EmailHandler - "../Security/OneTimeTokenHandler": @OneTimeTokenHandler - "../Analytics/AnalyticsManager": @AnalyticsManager = { recordEvent: sinon.stub() } - "settings-sharelatex": @settings = {siteUrl: "http://sl.example.com"} - "../Helpers/EmailHelper": EmailHelper - - @passingRequest = {email:"something@email.com", password:"123"} - - - describe 'validate Register Request', -> - it 'allows passing validation through', -> - result = @handler._registrationRequestIsValid @passingRequest - result.should.equal true - - describe 'failing email validation', -> - beforeEach -> - @AuthenticationManager.validateEmail.returns({ message: 'email not set' }) - - it 'does not allow through', -> - result = @handler._registrationRequestIsValid @passingRequest - result.should.equal false - - describe 'failing password validation', -> - beforeEach -> - @AuthenticationManager.validatePassword.returns({ message: 'password is too short' }) - - it 'does not allow through', -> - result = @handler._registrationRequestIsValid @passingRequest - result.should.equal false - - describe "registerNewUser", -> - - describe "holdingAccount", (done)-> - - beforeEach -> - @user.holdingAccount = true - @handler._registrationRequestIsValid = sinon.stub().returns true - @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @user) - - it "should not create a new user if there is a holding account there", (done)-> - @handler.registerNewUser @passingRequest, (err)=> - @UserCreator.createNewUser.called.should.equal false - done() - - it "should set holding account to false", (done)-> - @handler.registerNewUser @passingRequest, (err)=> - update = @User.update.args[0] - assert.deepEqual update[0], {_id:@user._id} - assert.deepEqual update[1], {"$set":{holdingAccount:false}} - done() - - describe "invalidRequest", -> - - it "should not create a new user if the the request is not valid", (done)-> - @handler._registrationRequestIsValid = sinon.stub().returns false - @handler.registerNewUser @passingRequest, (err)=> - expect(err).to.exist - @UserCreator.createNewUser.called.should.equal false - done() - - it "should return email registered in the error if there is a non holdingAccount there", (done)-> - @UserGetter.getUserByAnyEmail.callsArgWith(1, null, @user = {holdingAccount:false}) - @handler.registerNewUser @passingRequest, (err, user)=> - err.should.deep.equal new Error("EmailAlreadyRegistered") - user.should.deep.equal @user - done() - - describe "validRequest", -> - beforeEach -> - @handler._registrationRequestIsValid = sinon.stub().returns true - @UserGetter.getUserByAnyEmail.callsArgWith 1 - - it "should create a new user", (done)-> - @handler.registerNewUser @passingRequest, (err)=> - @UserCreator.createNewUser.calledWith({email:@passingRequest.email, holdingAccount:false, first_name:@passingRequest.first_name, last_name:@passingRequest.last_name}).should.equal true - done() - - it 'lower case email', (done)-> - @passingRequest.email = "soMe@eMail.cOm" - @handler.registerNewUser @passingRequest, (err)=> - @UserCreator.createNewUser.args[0][0].email.should.equal "some@email.com" - done() - - it 'trim white space from email', (done)-> - @passingRequest.email = " some@email.com " - @handler.registerNewUser @passingRequest, (err)=> - @UserCreator.createNewUser.args[0][0].email.should.equal "some@email.com" - done() - - - it "should set the password", (done)-> - @handler.registerNewUser @passingRequest, (err)=> - @AuthenticationManager.setUserPassword.calledWith(@user._id, @passingRequest.password).should.equal true - done() - - it "should add the user to the newsletter if accepted terms", (done)-> - @passingRequest.subscribeToNewsletter = "true" - @handler.registerNewUser @passingRequest, (err)=> - @NewsLetterManager.subscribe.calledWith(@user).should.equal true - done() - - it "should not add the user to the newsletter if not accepted terms", (done)-> - @handler.registerNewUser @passingRequest, (err)=> - @NewsLetterManager.subscribe.calledWith(@user).should.equal false - done() - - it "should track the registration event", (done)-> - @handler.registerNewUser @passingRequest, (err)=> - @AnalyticsManager.recordEvent - .calledWith(@user._id, "user-registered") - .should.equal true - done() - - - it "should call the ReferalAllocator", (done)-> - done() - - describe "registerNewUserAndSendActivationEmail", -> - beforeEach -> - @email = "email@example.com" - @crypto.randomBytes = sinon.stub().returns({toString: () => @password = "mock-password"}) - @OneTimeTokenHandler.getNewToken.yields(null, @token = "mock-token") - @handler.registerNewUser = sinon.stub() - @callback = sinon.stub() - - describe "with a new user", -> - beforeEach -> - @handler.registerNewUser.callsArgWith(1, null, @user) - @handler.registerNewUserAndSendActivationEmail @email, @callback - - it "should ask the UserRegistrationHandler to register user", -> - @handler.registerNewUser - .calledWith({ - email: @email - password: @password - }).should.equal true - - it "should generate a new password reset token", -> - - @OneTimeTokenHandler.getNewToken - .calledWith('password', @user_id, expiresIn: 7 * 24 * 60 * 60) - .should.equal true - - it "should send a registered email", -> - @EmailHandler.sendEmail - .calledWith("registered", { - to: @user.email - setNewPasswordUrl: "#{@settings.siteUrl}/user/activate?token=#{@token}&user_id=#{@user_id}" - }) - .should.equal true - - it "should return the user", -> - @callback - .calledWith(null, @user, "#{@settings.siteUrl}/user/activate?token=#{@token}&user_id=#{@user_id}") - .should.equal true - - describe "with a user that already exists", -> - beforeEach -> - @handler.registerNewUser.callsArgWith(1, new Error("EmailAlreadyRegistered"), @user) - @handler.registerNewUserAndSendActivationEmail @email, @callback - - it "should still generate a new password token and email", -> - @OneTimeTokenHandler.getNewToken.called.should.equal true - @EmailHandler.sendEmail.called.should.equal true \ No newline at end of file diff --git a/services/web/test/unit/coffee/User/UserSessionsManagerTests.coffee b/services/web/test/unit/coffee/User/UserSessionsManagerTests.coffee deleted file mode 100644 index b367bd893e..0000000000 --- a/services/web/test/unit/coffee/User/UserSessionsManagerTests.coffee +++ /dev/null @@ -1,576 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/Features/User/UserSessionsManager.js" -SandboxedModule = require('sandboxed-module') -Async = require('async') - -describe 'UserSessionsManager', -> - - beforeEach -> - @user = - _id: "abcd" - email: "user@example.com" - @sessionId = 'some_session_id' - - @rclient = - multi: sinon.stub() - exec: sinon.stub() - get: sinon.stub() - del: sinon.stub() - sadd: sinon.stub() - srem: sinon.stub() - smembers: sinon.stub() - mget: sinon.stub() - expire: sinon.stub() - @rclient.multi.returns(@rclient) - @rclient.get.returns(@rclient) - @rclient.del.returns(@rclient) - @rclient.sadd.returns(@rclient) - @rclient.srem.returns(@rclient) - @rclient.smembers.returns(@rclient) - @rclient.expire.returns(@rclient) - @rclient.exec.callsArgWith(0, null) - - @UserSessionsRedis = - client: () => @rclient - sessionSetKey: (user) => "UserSessions:{#{user._id}}" - @logger = - err: sinon.stub() - error: sinon.stub() - log: sinon.stub() - @settings = - redis: - web: {} - @UserSessionsManager = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger - "settings-sharelatex": @settings - './UserSessionsRedis': @UserSessionsRedis - 'async': Async - - describe '_sessionKey', -> - - it 'should build the correct key', -> - result = @UserSessionsManager._sessionKey(@sessionId) - result.should.equal 'sess:some_session_id' - - describe 'trackSession', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.trackSession @user, @sessionId, callback - @rclient.exec.callsArgWith(0, null) - @_checkSessions = sinon.stub(@UserSessionsManager, '_checkSessions').returns(null) - - afterEach -> - @_checkSessions.restore() - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - done() - - it 'should call the appropriate redis methods', (done) -> - @call (err) => - @rclient.multi.callCount.should.equal 1 - @rclient.sadd.callCount.should.equal 1 - @rclient.expire.callCount.should.equal 1 - @rclient.exec.callCount.should.equal 1 - done() - - it 'should call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 1 - done() - - describe 'when rclient produces an error', -> - - beforeEach -> - @rclient.exec.callsArgWith(0, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should not call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 0 - done() - - describe 'when no user is supplied', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.trackSession null, @sessionId, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not call the appropriate redis methods', (done) -> - @call (err) => - @rclient.multi.callCount.should.equal 0 - @rclient.sadd.callCount.should.equal 0 - @rclient.expire.callCount.should.equal 0 - @rclient.exec.callCount.should.equal 0 - done() - - it 'should not call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 0 - done() - - describe 'when no sessionId is supplied', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.trackSession @user, null, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not call the appropriate redis methods', (done) -> - @call (err) => - @rclient.multi.callCount.should.equal 0 - @rclient.sadd.callCount.should.equal 0 - @rclient.expire.callCount.should.equal 0 - @rclient.exec.callCount.should.equal 0 - done() - - it 'should not call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 0 - done() - - describe 'untrackSession', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.untrackSession @user, @sessionId, callback - @rclient.exec.callsArgWith(0, null) - @_checkSessions = sinon.stub(@UserSessionsManager, '_checkSessions').returns(null) - - afterEach -> - @_checkSessions.restore() - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal undefined - done() - - it 'should call the appropriate redis methods', (done) -> - @call (err) => - @rclient.multi.callCount.should.equal 1 - @rclient.srem.callCount.should.equal 1 - @rclient.expire.callCount.should.equal 1 - @rclient.exec.callCount.should.equal 1 - done() - - it 'should call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 1 - done() - - describe 'when rclient produces an error', -> - - beforeEach -> - @rclient.exec.callsArgWith(0, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should not call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 0 - done() - - describe 'when no user is supplied', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.untrackSession null, @sessionId, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not call the appropriate redis methods', (done) -> - @call (err) => - @rclient.multi.callCount.should.equal 0 - @rclient.srem.callCount.should.equal 0 - @rclient.expire.callCount.should.equal 0 - @rclient.exec.callCount.should.equal 0 - done() - - it 'should not call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 0 - done() - - describe 'when no sessionId is supplied', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.untrackSession @user, null, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not call the appropriate redis methods', (done) -> - @call (err) => - @rclient.multi.callCount.should.equal 0 - @rclient.srem.callCount.should.equal 0 - @rclient.expire.callCount.should.equal 0 - @rclient.exec.callCount.should.equal 0 - done() - - it 'should not call _checkSessions', (done) -> - @call (err) => - @_checkSessions.callCount.should.equal 0 - done() - - describe 'revokeAllUserSessions', -> - - beforeEach -> - @sessionKeys = ['sess:one', 'sess:two'] - @retain = [] - @rclient.smembers.callsArgWith(1, null, @sessionKeys) - @rclient.del = sinon.stub().callsArgWith(1, null) - @rclient.srem = sinon.stub().callsArgWith(2, null) - @call = (callback) => - @UserSessionsManager.revokeAllUserSessions @user, @retain, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should call the appropriate redis methods', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 1 - - @rclient.del.callCount.should.equal 2 - expect(@rclient.del.firstCall.args[0]).to.deep.equal @sessionKeys[0] - expect(@rclient.del.secondCall.args[0]).to.deep.equal @sessionKeys[1] - - @rclient.srem.callCount.should.equal 1 - expect(@rclient.srem.firstCall.args[1]).to.deep.equal @sessionKeys - - done() - - describe 'when a session is retained', -> - - beforeEach -> - @sessionKeys = ['sess:one', 'sess:two', 'sess:three', 'sess:four'] - @retain = ['two'] - @rclient.smembers.callsArgWith(1, null, @sessionKeys) - @rclient.del = sinon.stub().callsArgWith(1, null) - @call = (callback) => - @UserSessionsManager.revokeAllUserSessions @user, @retain, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should call the appropriate redis methods', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 1 - @rclient.del.callCount.should.equal @sessionKeys.length - 1 - @rclient.srem.callCount.should.equal 1 - done() - - it 'should remove all sessions except for the retained one', (done) -> - @call (err) => - expect(@rclient.del.firstCall.args[0]).to.deep.equal('sess:one') - expect(@rclient.del.secondCall.args[0]).to.deep.equal('sess:three') - expect(@rclient.del.thirdCall.args[0]).to.deep.equal('sess:four') - expect(@rclient.srem.firstCall.args[1]).to.deep.equal(['sess:one', 'sess:three', 'sess:four']) - done() - - describe 'when rclient produces an error', -> - - beforeEach -> - @rclient.del = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should not call rclient.srem', (done) -> - @call (err) => - @rclient.srem.callCount.should.equal 0 - done() - - describe 'when no user is supplied', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.revokeAllUserSessions null, @retain, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not call the appropriate redis methods', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 0 - @rclient.del.callCount.should.equal 0 - @rclient.srem.callCount.should.equal 0 - done() - - describe 'when there are no keys to delete', -> - - beforeEach -> - @rclient.smembers.callsArgWith(1, null, []) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not do the delete operation', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 1 - @rclient.del.callCount.should.equal 0 - @rclient.srem.callCount.should.equal 0 - done() - - describe 'touch', -> - - beforeEach -> - @rclient.expire.callsArgWith(2, null) - @call = (callback) => - @UserSessionsManager.touch @user, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should call rclient.expire', (done) -> - @call (err) => - @rclient.expire.callCount.should.equal 1 - done() - - describe 'when rclient produces an error', -> - - beforeEach -> - @rclient.expire.callsArgWith(2, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - describe 'when no user is supplied', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager.touch null, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not call expire', (done) -> - @call (err) => - @rclient.expire.callCount.should.equal 0 - done() - - describe 'getAllUserSessions', -> - - beforeEach -> - @sessionKeys = ['sess:one', 'sess:two', 'sess:three'] - @sessions = [ - '{"user": {"ip_address": "a", "session_created": "b"}}', - '{"passport": {"user": {"ip_address": "c", "session_created": "d"}}}' - ] - @exclude = ['two'] - @rclient.smembers.callsArgWith(1, null, @sessionKeys) - @rclient.get = sinon.stub() - @rclient.get.onCall(0).callsArgWith(1, null, @sessions[0]) - @rclient.get.onCall(1).callsArgWith(1, null, @sessions[1]) - - @call = (callback) => - @UserSessionsManager.getAllUserSessions @user, @exclude, callback - - it 'should not produce an error', (done) -> - @call (err, sessions) => - expect(err).to.equal null - done() - - it 'should get sessions', (done) -> - @call (err, sessions) => - expect(sessions).to.deep.equal [ - { ip_address: 'a', session_created: 'b' }, - { ip_address: 'c', session_created: 'd' } - ] - done() - - it 'should have called rclient.smembers', (done) -> - @call (err, sessions) => - @rclient.smembers.callCount.should.equal 1 - done() - - it 'should have called rclient.get', (done) -> - @call (err, sessions) => - @rclient.get.callCount.should.equal @sessionKeys.length - 1 - done() - - describe 'when there are no other sessions', -> - - beforeEach -> - @sessionKeys = ['sess:two'] - @rclient.smembers.callsArgWith(1, null, @sessionKeys) - - it 'should not produce an error', (done) -> - @call (err, sessions) => - expect(err).to.equal null - done() - - it 'should produce an empty list of sessions', (done) -> - @call (err, sessions) => - expect(sessions).to.deep.equal [] - done() - - it 'should have called rclient.smembers', (done) -> - @call (err, sessions) => - @rclient.smembers.callCount.should.equal 1 - done() - - it 'should not have called rclient.mget', (done) -> - @call (err, sessions) => - @rclient.mget.callCount.should.equal 0 - done() - - describe 'when smembers produces an error', -> - - beforeEach -> - @rclient.smembers.callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, sessions) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - it 'should not have called rclient.mget', (done) -> - @call (err, sessions) => - @rclient.mget.callCount.should.equal 0 - done() - - describe 'when get produces an error', -> - - beforeEach -> - @rclient.get = sinon.stub().callsArgWith(1, new Error('woops')) - - it 'should produce an error', (done) -> - @call (err, sessions) => - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() - - - describe '_checkSessions', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager._checkSessions @user, callback - @sessionKeys = ['one', 'two'] - @rclient.smembers.callsArgWith(1, null, @sessionKeys) - @rclient.get.callsArgWith(1, null, 'some-value') - @rclient.srem.callsArgWith(2, null, {}) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal undefined - done() - - it 'should call the appropriate redis methods', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 1 - @rclient.get.callCount.should.equal 2 - @rclient.srem.callCount.should.equal 0 - done() - - describe 'when one of the keys is not present in redis', -> - - beforeEach -> - @rclient.get.onCall(0).callsArgWith(1, null, 'some-val') - @rclient.get.onCall(1).callsArgWith(1, null, null) - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal undefined - done() - - it 'should remove that key from the set', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 1 - @rclient.get.callCount.should.equal 2 - @rclient.srem.callCount.should.equal 1 - @rclient.srem.firstCall.args[1].should.equal 'two' - done() - - describe 'when no user is supplied', -> - - beforeEach -> - @call = (callback) => - @UserSessionsManager._checkSessions null, callback - - it 'should not produce an error', (done) -> - @call (err) => - expect(err).to.not.be.instanceof Error - expect(err).to.equal null - done() - - it 'should not call redis methods', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 0 - @rclient.get.callCount.should.equal 0 - done() - - describe 'when one of the get operations produces an error', -> - - beforeEach -> - @rclient.get.onCall(0).callsArgWith(1, new Error('woops'), null) - @rclient.get.onCall(1).callsArgWith(1, null, null) - - it 'should produce an error', (done) -> - @call (err) => - expect(err).to.be.instanceof Error - done() - - it 'should call the right redis methods, bailing out early', (done) -> - @call (err) => - @rclient.smembers.callCount.should.equal 1 - @rclient.get.callCount.should.equal 1 - @rclient.srem.callCount.should.equal 0 - done() diff --git a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee deleted file mode 100644 index ff52dbb804..0000000000 --- a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee +++ /dev/null @@ -1,278 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/User/UserUpdater" -expect = require("chai").expect -tk = require('timekeeper') - -describe "UserUpdater", -> - - beforeEach -> - tk.freeze(Date.now()) - @mongojs = - db:{} - ObjectId:(id)-> return id - @UserGetter = - getUserEmail: sinon.stub() - getUserByAnyEmail: sinon.stub() - ensureUniqueEmailAddress: sinon.stub() - @logger = - err: sinon.stub() - log: -> - warn: -> - @addAffiliation = sinon.stub().yields() - @removeAffiliation = sinon.stub().callsArgWith(2, null) - @refreshFeatures = sinon.stub().yields() - @NewsletterManager = - changeEmail:sinon.stub() - @UserUpdater = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger - "../../infrastructure/mongojs":@mongojs - "metrics-sharelatex": timeAsyncMethod: sinon.stub() - "./UserGetter": @UserGetter - '../Institutions/InstitutionsAPI': - addAffiliation: @addAffiliation - removeAffiliation: @removeAffiliation - '../Subscription/FeaturesUpdater': refreshFeatures: @refreshFeatures - "settings-sharelatex": @settings = {} - "request": @request = {} - "../Newsletter/NewsletterManager": @NewsletterManager - - @stubbedUser = - _id: "3131231" - name:"bob" - email:"hello@world.com" - @newEmail = "bob@bob.com" - - afterEach -> - tk.reset() - - describe 'changeEmailAddress', -> - beforeEach -> - @UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email) - @UserUpdater.addEmailAddress = sinon.stub().callsArgWith(2) - @UserUpdater.setDefaultEmailAddress = sinon.stub().callsArgWith(2) - @UserUpdater.removeEmailAddress = sinon.stub().callsArgWith(2) - - it 'change email', (done)-> - @UserUpdater.changeEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @UserUpdater.addEmailAddress.calledWith( - @stubbedUser._id, @newEmail - ).should.equal true - @UserUpdater.setDefaultEmailAddress.calledWith( - @stubbedUser._id, @newEmail - ).should.equal true - @UserUpdater.removeEmailAddress.calledWith( - @stubbedUser._id, @stubbedUser.email - ).should.equal true - done() - - it 'validates email', (done)-> - @UserUpdater.changeEmailAddress @stubbedUser._id, 'foo', (err)=> - should.exist(err) - done() - - it 'handle error', (done)-> - @UserUpdater.removeEmailAddress.callsArgWith(2, new Error('nope')) - @UserUpdater.changeEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - done() - - describe 'addEmailAddress', -> - beforeEach -> - @UserGetter.ensureUniqueEmailAddress = sinon.stub().callsArgWith(1) - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null) - - it 'add email', (done)-> - @UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, (err)=> - @UserGetter.ensureUniqueEmailAddress.called.should.equal true - should.not.exist(err) - reversedHostname = @newEmail.split('@')[1].split('').reverse().join('') - @UserUpdater.updateUser.calledWith( - @stubbedUser._id, - $push: { emails: { email: @newEmail, createdAt: sinon.match.date, reversedHostname: reversedHostname } } - ).should.equal true - done() - - it 'add affiliation', (done)-> - affiliationOptions = - university: { id: 1 } - role: 'Prof' - department: 'Math' - @UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, affiliationOptions, (err)=> - should.not.exist(err) - @addAffiliation.calledOnce.should.equal true - args = @addAffiliation.lastCall.args - args[0].should.equal @stubbedUser._id - args[1].should.equal @newEmail - args[2].should.equal affiliationOptions - done() - - it 'handle error', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope')) - - @UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, (err)=> - @logger.err.called.should.equal true - should.exist(err) - done() - - it 'handle affiliation error', (done)-> - body = errors: 'affiliation error message' - @addAffiliation.callsArgWith(3, new Error('nope')) - @UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - @UserUpdater.updateUser.called.should.equal false - done() - - it 'validates email', (done)-> - @UserUpdater.addEmailAddress @stubbedUser._id, 'bar', (err)=> - should.exist(err) - done() - - describe 'removeEmailAddress', -> - beforeEach -> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 1) - - it 'remove email', (done)-> - @UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @UserUpdater.updateUser.calledWith( - { _id: @stubbedUser._id, email: { $ne: @newEmail } }, - $pull: { emails: { email: @newEmail } } - ).should.equal true - done() - - it 'remove affiliation', (done)-> - @UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @removeAffiliation.calledOnce.should.equal true - args = @removeAffiliation.lastCall.args - args[0].should.equal @stubbedUser._id - args[1].should.equal @newEmail - done() - - it 'handle error', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope')) - - @UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - done() - - it 'handle missed update', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0) - - @UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - done() - - it 'handle affiliation error', (done)-> - @removeAffiliation.callsArgWith(2, new Error('nope')) - @UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - @UserUpdater.updateUser.called.should.equal false - done() - - it 'validates email', (done)-> - @UserUpdater.removeEmailAddress @stubbedUser._id, 'baz', (err)=> - should.exist(err) - done() - - describe 'setDefaultEmailAddress', -> - beforeEach -> - @UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email) - @NewsletterManager.changeEmail.callsArgWith(2, null) - - it 'set default', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1) - - @UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @UserUpdater.updateUser.calledWith( - { _id: @stubbedUser._id, 'emails.email': @newEmail }, - $set: { email: @newEmail } - ).should.equal true - done() - - it 'set changed the email in newsletter', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1) - - @UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @NewsletterManager.changeEmail.calledWith( - @stubbedUser.email, @newEmail - ).should.equal true - done() - - it 'handle error', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope')) - - @UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - done() - - it 'handle missed update', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0) - - @UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - done() - - it 'validates email', (done)-> - @UserUpdater.setDefaultEmailAddress @stubbedUser._id, '.edu', (err)=> - should.exist(err) - done() - - describe 'confirmEmail', -> - beforeEach -> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1) - - it 'should update the email record', (done)-> - @UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @UserUpdater.updateUser.calledWith( - { _id: @stubbedUser._id, 'emails.email': @newEmail }, - $set: { 'emails.$.confirmedAt': new Date() } - ).should.equal true - done() - - it 'add affiliation', (done)-> - @UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - @addAffiliation.calledOnce.should.equal true - sinon.assert.calledWith(@addAffiliation, @stubbedUser._id, @newEmail, { confirmedAt: new Date() } ) - done() - - it 'handle error', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope')) - - @UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - done() - - it 'handle missed update', (done)-> - @UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0) - - @UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - done() - - it 'validates email', (done)-> - @UserUpdater.confirmEmail @stubbedUser._id, '@', (err)=> - should.exist(err) - done() - - it 'handle affiliation error', (done)-> - @addAffiliation.callsArgWith(3, new Error('nope')) - @UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=> - should.exist(err) - @UserUpdater.updateUser.called.should.equal false - done() - - it 'refresh features', (done)-> - @UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=> - should.not.exist(err) - sinon.assert.calledWith(@refreshFeatures, @stubbedUser._id, true) - done() \ No newline at end of file diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee deleted file mode 100644 index 5f47ce1aa5..0000000000 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee +++ /dev/null @@ -1,183 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -expect = require('chai').expect -modulePath = "../../../../app/js/Features/UserMembership/UserMembershipAuthorization.js" -SandboxedModule = require('sandboxed-module') -MockRequest = require "../helpers/MockRequest" -EntityConfigs = require("../../../../app/js/Features/UserMembership/UserMembershipEntityConfigs") -Errors = require("../../../../app/js/Features/Errors/Errors") - -describe "UserMembershipAuthorization", -> - beforeEach -> - @req = new MockRequest() - @req.params.id = 'mock-entity-id' - @user = _id: 'mock-user-id' - @subscription = { _id: 'mock-subscription-id'} - - @AuthenticationController = - getSessionUser: sinon.stub().returns(@user) - @UserMembershipHandler = - getEntity: sinon.stub().yields(null, @subscription) - getEntityWithoutAuthorizationCheck: sinon.stub().yields(null, @subscription) - @AuthorizationMiddleware = - redirectToRestricted: sinon.stub().yields() - ensureUserIsSiteAdmin: sinon.stub().yields() - @UserMembershipAuthorization = SandboxedModule.require modulePath, requires: - '../Authentication/AuthenticationController': @AuthenticationController - '../Authorization/AuthorizationMiddleware': @AuthorizationMiddleware - './UserMembershipHandler': @UserMembershipHandler - './EntityConfigs': EntityConfigs - '../Errors/Errors': Errors - 'request': @request = sinon.stub().yields(null, null, {}) - "logger-sharelatex": - log: -> - err: -> - - describe 'requireAccessToEntity', -> - it 'get entity', (done) -> - @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => - expect(error).to.not.extist - sinon.assert.calledWithMatch( - @UserMembershipHandler.getEntity, - @req.params.id, - modelName: 'Subscription', - @user - ) - expect(@req.entity).to.equal @subscription - expect(@req.entityConfig).to.exist - done() - - it 'handle entity not found as non-admin', (done) -> - @UserMembershipHandler.getEntity.yields(null, null) - @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) - @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => - expect(error).to.extist - expect(error).to.be.instanceof(Error) - expect(error.constructor.name).to.equal('NotFoundError') - sinon.assert.called(@UserMembershipHandler.getEntity) - expect(@req.entity).to.not.exist - done() - - it 'handle entity not found an admin can create', (done) -> - @user.isAdmin = true - @UserMembershipHandler.getEntity.yields(null, null) - @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) - @UserMembershipAuthorization.requirePublisherMetricsAccess @req, redirect: (path) => - expect(path).to.extist - expect(path).to.match /create/ - done() - - it 'handle entity not found a non-admin can create', (done) -> - @user.staffAccess = { institutionManagement: true } - @UserMembershipHandler.getEntity.yields(null, null) - @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) - @UserMembershipAuthorization.requirePublisherMetricsAccess @req, redirect: (path) => - expect(path).to.extist - expect(path).to.match /create/ - done() - - it 'handle entity not found an admin cannot create', (done) -> - @user.isAdmin = true - @UserMembershipHandler.getEntity.yields(null, null) - @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) - @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => - expect(error).to.extist - expect(error).to.be.instanceof(Error) - expect(error.constructor.name).to.equal('NotFoundError') - done() - - it 'handle entity no access', (done) -> - @UserMembershipHandler.getEntity.yields(null, null) - @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => - sinon.assert.called(@AuthorizationMiddleware.redirectToRestricted) - done() - - it 'handle anonymous user', (done) -> - @AuthenticationController.getSessionUser.returns(null) - @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => - expect(error).to.extist - sinon.assert.called(@AuthorizationMiddleware.redirectToRestricted) - sinon.assert.notCalled(@UserMembershipHandler.getEntity) - expect(@req.entity).to.not.exist - done() - - describe 'requireEntityAccess', -> - it 'handle team access', (done) -> - @UserMembershipAuthorization.requireTeamMetricsAccess @req, null, (error) => - expect(error).to.not.extist - sinon.assert.calledWithMatch( - @UserMembershipHandler.getEntity, - @req.params.id, - fields: primaryKey: 'overleaf.id' - ) - done() - - it 'handle group access', (done) -> - @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => - expect(error).to.not.extist - sinon.assert.calledWithMatch( - @UserMembershipHandler.getEntity, - @req.params.id, - translations: title: 'group_account' - ) - done() - - it 'handle group managers access', (done) -> - @UserMembershipAuthorization.requireGroupManagersManagementAccess @req, null, (error) => - expect(error).to.not.extist - sinon.assert.calledWithMatch( - @UserMembershipHandler.getEntity, - @req.params.id, - translations: subtitle: 'managers_management' - ) - done() - - it 'handle institution access', (done) -> - @UserMembershipAuthorization.requireInstitutionMetricsAccess @req, null, (error) => - expect(error).to.not.extist - sinon.assert.calledWithMatch( - @UserMembershipHandler.getEntity, - @req.params.id, - modelName: 'Institution', - ) - done() - - it 'handle template with brand access', (done) -> - templateData = - id: 123 - title: 'Template Title' - brand: { slug: 'brand-slug' } - @request.yields(null, { statusCode: 200 }, JSON.stringify(templateData)) - @UserMembershipAuthorization.requireTemplateMetricsAccess @req, null, (error) => - expect(error).to.not.extist - sinon.assert.calledWithMatch( - @UserMembershipHandler.getEntity, - 'brand-slug', - modelName: 'Publisher', - ) - done() - - it 'handle template without brand access', (done) -> - templateData = - id: 123 - title: 'Template Title' - brand: null - @request.yields(null, { statusCode: 200 }, JSON.stringify(templateData)) - @UserMembershipAuthorization.requireTemplateMetricsAccess @req, null, (error) => - expect(error).to.not.extist - sinon.assert.notCalled(@UserMembershipHandler.getEntity) - sinon.assert.calledOnce(@AuthorizationMiddleware.ensureUserIsSiteAdmin) - done() - - it 'handle graph access', (done) -> - @req.query.resource_id = 'mock-resource-id' - @req.query.resource_type = 'institution' - middleware = @UserMembershipAuthorization.requireGraphAccess - middleware @req, null, (error) => - expect(error).to.not.extist - sinon.assert.calledWithMatch( - @UserMembershipHandler.getEntity, - @req.query.resource_id, - modelName: 'Institution', - ) - done() diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipControllerTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipControllerTests.coffee deleted file mode 100644 index e2f11e7f92..0000000000 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipControllerTests.coffee +++ /dev/null @@ -1,241 +0,0 @@ -sinon = require('sinon') -assertCalledWith = sinon.assert.calledWith -assertNotCalled = sinon.assert.notCalled -chai = require('chai') -should = chai.should() -assert = chai.assert -expect = require('chai').expect -modulePath = "../../../../app/js/Features/UserMembership/UserMembershipController.js" -SandboxedModule = require('sandboxed-module') -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" -EntityConfigs = require("../../../../app/js/Features/UserMembership/UserMembershipEntityConfigs") -Errors = require("../../../../app/js/Features/Errors/Errors") - -describe "UserMembershipController", -> - beforeEach -> - @req = new MockRequest() - @req.params.id = 'mock-entity-id' - @user = _id: 'mock-user-id' - @newUser = _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' - @subscription = - _id: 'mock-subscription-id' - fetchV1Data: (callback) => callback(null, @subscription) - @institution = - _id: 'mock-institution-id' - v1Id: 123 - fetchV1Data: (callback) => - institution = Object.assign({}, @institution) - institution.name = 'Test Institution Name' - callback(null, institution) - @users = [ - { _id: 'mock-member-id-1', email: 'mock-email-1@foo.com' } - { _id: 'mock-member-id-2', email: 'mock-email-2@foo.com' } - ] - - @AuthenticationController = - getSessionUser: sinon.stub().returns(@user) - getLoggedInUserId: sinon.stub().returns(@user._id) - @UserMembershipHandler = - getEntity: sinon.stub().yields(null, @subscription) - createEntity: sinon.stub().yields(null, @institution) - getUsers: sinon.stub().yields(null, @users) - addUser: sinon.stub().yields(null, @newUser) - removeUser: sinon.stub().yields(null) - @UserMembershipController = SandboxedModule.require modulePath, requires: - '../Authentication/AuthenticationController': @AuthenticationController - './UserMembershipHandler': @UserMembershipHandler - '../Errors/Errors': Errors - "logger-sharelatex": - log: -> - err: -> - - describe 'index', -> - beforeEach -> - @req.entity = @subscription - @req.entityConfig = EntityConfigs.group - - it 'get users', (done) -> - @UserMembershipController.index @req, render: () => - sinon.assert.calledWithMatch( - @UserMembershipHandler.getUsers, - @subscription, - modelName: 'Subscription', - ) - done() - - it 'render group view', (done) -> - @UserMembershipController.index @req, render: (viewPath, viewParams) => - expect(viewPath).to.equal 'user_membership/index' - expect(viewParams.users).to.deep.equal @users - expect(viewParams.groupSize).to.equal @subscription.membersLimit - expect(viewParams.translations.title).to.equal 'group_account' - expect(viewParams.paths.addMember).to.equal "/manage/groups/#{@subscription._id}/invites" - done() - - it 'render group managers view', (done) -> - @req.entityConfig = EntityConfigs.groupManagers - @UserMembershipController.index @req, render: (viewPath, viewParams) => - expect(viewPath).to.equal 'user_membership/index' - expect(viewParams.groupSize).to.equal undefined - expect(viewParams.translations.title).to.equal 'group_account' - expect(viewParams.translations.subtitle).to.equal 'managers_management' - expect(viewParams.paths.exportMembers).to.be.undefined - done() - - it 'render institution view', (done) -> - @req.entity = @institution - @req.entityConfig = EntityConfigs.institution - @UserMembershipController.index @req, render: (viewPath, viewParams) => - expect(viewPath).to.equal 'user_membership/index' - expect(viewParams.name).to.equal 'Test Institution Name' - expect(viewParams.groupSize).to.equal undefined - expect(viewParams.translations.title).to.equal 'institution_account' - expect(viewParams.paths.exportMembers).to.be.undefined - done() - - describe 'add', -> - beforeEach -> - @req.body.email = @newUser.email - @req.entity = @subscription - @req.entityConfig = EntityConfigs.groupManagers - - it 'add user', (done) -> - @UserMembershipController.add @req, json: () => - sinon.assert.calledWithMatch( - @UserMembershipHandler.addUser, - @subscription, - modelName: 'Subscription', - @newUser.email - ) - done() - - it 'return user object', (done) -> - @UserMembershipController.add @req, json: (payload) => - payload.user.should.equal @newUser - done() - - it 'handle readOnly entity', (done) -> - @req.entityConfig = EntityConfigs.group - @UserMembershipController.add @req, null, (error) => - expect(error).to.extist - expect(error).to.be.an.instanceof(Errors.NotFoundError) - done() - - it 'handle user already added', (done) -> - @UserMembershipHandler.addUser.yields(alreadyAdded: true) - @UserMembershipController.add @req, status: () => json: (payload) => - expect(payload.error.code).to.equal 'user_already_added' - done() - - it 'handle user not found', (done) -> - @UserMembershipHandler.addUser.yields(userNotFound: true) - @UserMembershipController.add @req, status: () => json: (payload) => - expect(payload.error.code).to.equal 'user_not_found' - done() - - it 'handle invalid email', (done) -> - @req.body.email = 'not_valid_email' - @UserMembershipController.add @req, status: () => json: (payload) => - expect(payload.error.code).to.equal 'invalid_email' - done() - - describe 'remove', -> - beforeEach -> - @req.params.userId = @newUser._id - @req.entity = @subscription - @req.entityConfig = EntityConfigs.groupManagers - - it 'remove user', (done) -> - @UserMembershipController.remove @req, send: () => - sinon.assert.calledWithMatch( - @UserMembershipHandler.removeUser, - @subscription, - modelName: 'Subscription', - @newUser._id - ) - done() - - it 'handle readOnly entity', (done) -> - @req.entityConfig = EntityConfigs.group - @UserMembershipController.remove @req, null, (error) => - expect(error).to.extist - expect(error).to.be.an.instanceof(Errors.NotFoundError) - done() - - it 'prevent self removal', (done) -> - @req.params.userId = @user._id - @UserMembershipController.remove @req, status: () => json: (payload) => - expect(payload.error.code).to.equal 'managers_cannot_remove_self' - done() - - it 'prevent admin removal', (done) -> - @UserMembershipHandler.removeUser.yields(isAdmin: true) - @UserMembershipController.remove @req, status: () => json: (payload) => - expect(payload.error.code).to.equal 'managers_cannot_remove_admin' - done() - - describe "exportCsv", -> - - beforeEach -> - @req.entity = @subscription - @req.entityConfig = EntityConfigs.groupManagers - @res = new MockResponse() - @res.contentType = sinon.stub() - @res.header = sinon.stub() - @res.send = sinon.stub() - @UserMembershipController.exportCsv @req, @res - - it 'get users', -> - sinon.assert.calledWithMatch( - @UserMembershipHandler.getUsers, - @subscription, - modelName: 'Subscription', - ) - - it "should set the correct content type on the request", -> - assertCalledWith(@res.contentType, "text/csv") - - it "should name the exported csv file", -> - assertCalledWith( - @res.header - "Content-Disposition", - "attachment; filename=Group.csv" - ) - - it "should export the correct csv", -> - assertCalledWith(@res.send, "mock-email-1@foo.com\nmock-email-2@foo.com\n") - - describe 'new', -> - beforeEach -> - @req.params.name = 'publisher' - @req.params.id = 'abc' - - it 'renders view', (done) -> - @UserMembershipController.new @req, render: (viewPath, data) => - expect(data.entityName).to.eq 'publisher' - expect(data.entityId).to.eq 'abc' - done() - - describe 'create', -> - beforeEach -> - @req.params.name = 'institution' - @req.params.id = 123 - - it 'creates institution', (done) -> - @UserMembershipController.create @req, redirect: (path) => - expect(path).to.eq EntityConfigs['institution'].pathsFor(123).index - sinon.assert.calledWithMatch( - @UserMembershipHandler.createEntity, - 123, - modelName: 'Institution', - ) - done() - - it 'checks canCreate', (done) -> - @req.params.name = 'group' - @UserMembershipController.create @req, null, (error) => - expect(error).to.extist - expect(error).to.be.an.instanceof(Errors.NotFoundError) - sinon.assert.notCalled(@UserMembershipHandler.createEntity) - done() diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee deleted file mode 100644 index ae4d702afa..0000000000 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee +++ /dev/null @@ -1,212 +0,0 @@ -chai = require('chai') -should = chai.should() -expect = require('chai').expect -sinon = require('sinon') -assertCalledWith = sinon.assert.calledWith -assertNotCalled = sinon.assert.notCalled -ObjectId = require("../../../../app/js/infrastructure/mongojs").ObjectId -modulePath = "../../../../app/js/Features/UserMembership/UserMembershipHandler" -SandboxedModule = require("sandboxed-module") -Errors = require("../../../../app/js/Features/Errors/Errors") -EntityConfigs = require("../../../../app/js/Features/UserMembership/UserMembershipEntityConfigs") - -describe 'UserMembershipHandler', -> - beforeEach -> - @user = _id: ObjectId() - @newUser = _id: ObjectId(), email: 'new-user-email@foo.bar' - @fakeEntityId = ObjectId() - @subscription = - _id: 'mock-subscription-id' - groupPlan: true - membersLimit: 10 - member_ids: [ObjectId(), ObjectId()] - manager_ids: [ObjectId()] - invited_emails: ['mock-email-1@foo.com'] - teamInvites: [{ email: 'mock-email-1@bar.com' }] - update: sinon.stub().yields(null) - @institution = - _id: 'mock-institution-id' - v1Id: 123 - managerIds: [ObjectId(), ObjectId(), ObjectId()] - update: sinon.stub().yields(null) - @publisher = - _id: 'mock-publisher-id' - slug: 'slug' - managerIds: [ObjectId(), ObjectId()] - update: sinon.stub().yields(null) - - @UserMembershipViewModel = - buildAsync: sinon.stub().yields(null, { _id: 'mock-member-id'}) - build: sinon.stub().returns(@newUser) - @UserGetter = - getUserByAnyEmail: sinon.stub().yields(null, @newUser) - @Institution = - findOne: sinon.stub().yields(null, @institution) - @Subscription = - findOne: sinon.stub().yields(null, @subscription) - @Publisher = - findOne: sinon.stub().yields(null, @publisher) - create: sinon.stub().yields(null, @publisher) - @UserMembershipHandler = SandboxedModule.require modulePath, requires: - './UserMembershipViewModel': @UserMembershipViewModel - '../User/UserGetter': @UserGetter - '../Errors/Errors': Errors - '../../models/Institution': Institution: @Institution - '../../models/Subscription': Subscription: @Subscription - '../../models/Publisher': Publisher: @Publisher - 'logger-sharelatex': - log: -> - err: -> - - describe 'getEntity', -> - describe 'group subscriptions', -> - it 'get subscription', (done) -> - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, @user, null, (error, subscription) => - should.not.exist(error) - expectedQuery = - groupPlan: true - _id: @fakeEntityId - manager_ids: ObjectId(@user._id) - assertCalledWith(@Subscription.findOne, expectedQuery) - expect(subscription).to.equal @subscription - expect(subscription.membersLimit).to.equal 10 - done() - - it 'get for admin', (done) -> - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, { isAdmin: true }, null, (error, subscription) => - should.not.exist(error) - expectedQuery = - groupPlan: true - _id: @fakeEntityId - assertCalledWith(@Subscription.findOne, expectedQuery) - done() - - it 'get with staffAccess field', (done) -> - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, { staffAccess: {institutionMetrics: true}}, 'institutionMetrics', (error, subscription) => - should.not.exist(error) - expectedQuery = - groupPlan: true - _id: @fakeEntityId - assertCalledWith(@Subscription.findOne, expectedQuery) - done() - - it 'handle error', (done) -> - @Subscription.findOne.yields(new Error('some error')) - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, @user._id, null, (error, subscription) => - should.exist(error) - done() - - describe 'getEntityWithoutAuthorizationCheck', -> - it 'get publisher', (done) -> - @UserMembershipHandler.getEntityWithoutAuthorizationCheck @fakeEntityId, EntityConfigs.publisher, (error, subscription) => - should.not.exist(error) - expectedQuery = slug: @fakeEntityId - assertCalledWith(@Publisher.findOne, expectedQuery) - expect(subscription).to.equal @publisher - done() - - describe 'institutions', -> - it 'get institution', (done) -> - @UserMembershipHandler.getEntity @institution.v1Id, EntityConfigs.institution, @user, null, (error, institution) => - should.not.exist(error) - expectedQuery = v1Id: @institution.v1Id, managerIds: ObjectId(@user._id) - assertCalledWith(@Institution.findOne, expectedQuery) - expect(institution).to.equal @institution - done() - - it 'handle errors', (done) -> - @Institution.findOne.yields(new Error('nope')) - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.institution, @user._id, null, (error, institution) => - should.exist(error) - expect(error).to.not.be.an.instanceof(Errors.NotFoundError) - done() - - describe 'publishers', -> - it 'get publisher', (done) -> - @UserMembershipHandler.getEntity @publisher.slug, EntityConfigs.publisher, @user, null, (error, institution) => - should.not.exist(error) - expectedQuery = slug: @publisher.slug, managerIds: ObjectId(@user._id) - assertCalledWith(@Publisher.findOne, expectedQuery) - expect(institution).to.equal @publisher - done() - - describe 'getUsers', -> - describe 'group', -> - it 'build view model for all users', (done) -> - @UserMembershipHandler.getUsers @subscription, EntityConfigs.group, (error, users) => - expectedCallcount = - @subscription.member_ids.length + - @subscription.invited_emails.length + - @subscription.teamInvites.length - expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount - done() - - describe 'group mamagers', -> - it 'build view model for all managers', (done) -> - @UserMembershipHandler.getUsers @subscription, EntityConfigs.groupManagers, (error, users) => - expectedCallcount = @subscription.manager_ids.length - expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount - done() - - describe 'institution', -> - it 'build view model for all managers', (done) -> - @UserMembershipHandler.getUsers @institution, EntityConfigs.institution, (error, users) => - expectedCallcount = @institution.managerIds.length - expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount - done() - - describe 'createEntity', -> - it 'creates publisher', (done) -> - @UserMembershipHandler.createEntity @fakeEntityId, EntityConfigs.publisher, (error, publisher) => - should.not.exist(error) - assertCalledWith(@Publisher.create, slug: @fakeEntityId) - done() - - describe 'addUser', -> - beforeEach -> - @email = @newUser.email - - describe 'institution', -> - it 'get user', (done) -> - @UserMembershipHandler.addUser @institution, EntityConfigs.institution, @email, (error, user) => - assertCalledWith(@UserGetter.getUserByAnyEmail, @email) - done() - - it 'handle user not found', (done) -> - @UserGetter.getUserByAnyEmail.yields(null, null) - @UserMembershipHandler.addUser @institution, EntityConfigs.institution, @email, (error) => - expect(error).to.exist - expect(error.userNotFound).to.equal true - done() - - it 'handle user already added', (done) -> - @institution.managerIds.push(@newUser._id) - @UserMembershipHandler.addUser @institution, EntityConfigs.institution, @email, (error, users) => - expect(error).to.exist - expect(error.alreadyAdded).to.equal true - done() - - it 'add user to institution', (done) -> - @UserMembershipHandler.addUser @institution, EntityConfigs.institution, @email, (error, user) => - assertCalledWith(@institution.update, { $addToSet: managerIds: @newUser._id }) - done() - - it 'return user view', (done) -> - @UserMembershipHandler.addUser @institution, EntityConfigs.institution, @email, (error, user) => - user.should.equal @newUser - done() - - describe 'removeUser', -> - describe 'institution', -> - it 'remove user from institution', (done) -> - @UserMembershipHandler.removeUser @institution, EntityConfigs.institution, @newUser._id, (error, user) => - lastCall = @institution.update.lastCall - assertCalledWith(@institution.update, { $pull: managerIds: @newUser._id }) - done() - - it 'handle admin', (done) -> - @subscription.admin_id = @newUser._id - @UserMembershipHandler.removeUser @subscription, EntityConfigs.groupManagers, @newUser._id, (error, user) => - expect(error).to.exist - expect(error.isAdmin).to.equal true - done() diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipViewModelTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipViewModelTests.coffee deleted file mode 100644 index 7dc7dbeff0..0000000000 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipViewModelTests.coffee +++ /dev/null @@ -1,83 +0,0 @@ -chai = require('chai') -should = chai.should() -expect = require('chai').expect -sinon = require('sinon') -assertCalledWith = sinon.assert.calledWith -assertNotCalled = sinon.assert.notCalled -mongojs = require('mongojs') -ObjectId = mongojs.ObjectId -modulePath = "../../../../app/js/Features/UserMembership/UserMembershipViewModel" -SandboxedModule = require("sandboxed-module") - -describe 'UserMembershipViewModel', -> - beforeEach -> - @UserGetter = - getUserOrUserStubById: sinon.stub() - @UserMembershipViewModel = SandboxedModule.require modulePath, requires: - 'mongojs': mongojs - '../User/UserGetter': @UserGetter - @email = 'mock-email@bar.com' - @user = _id: 'mock-user-id', email: 'mock-email@baz.com', first_name: 'Name' - @userStub = _id: 'mock-user-stub-id', email: 'mock-stub-email@baz.com' - - describe 'build', -> - it 'build email', -> - viewModel = @UserMembershipViewModel.build(@email) - expect(viewModel).to.deep.equal - email: @email - invite: true - first_name: null - last_name: null - _id: null - - it 'build user', -> - viewModel = @UserMembershipViewModel.build(@user) - expect(viewModel._id).to.equal @user._id - expect(viewModel.email).to.equal @user.email - expect(viewModel.invite).to.equal false - - describe 'build async', -> - beforeEach -> - @UserMembershipViewModel.build = sinon.stub() - - it 'build email', (done) -> - @UserMembershipViewModel.buildAsync @email, (error, viewModel) => - assertCalledWith(@UserMembershipViewModel.build, @email) - done() - - it 'build user', (done) -> - @UserMembershipViewModel.buildAsync @user, (error, viewModel) => - assertCalledWith(@UserMembershipViewModel.build, @user) - done() - - it 'build user id', (done) -> - @UserGetter.getUserOrUserStubById.yields(null, @user, false) - @UserMembershipViewModel.buildAsync ObjectId(), (error, viewModel) => - should.not.exist(error) - assertNotCalled(@UserMembershipViewModel.build) - expect(viewModel._id).to.equal @user._id - expect(viewModel.email).to.equal @user.email - expect(viewModel.first_name).to.equal @user.first_name - expect(viewModel.invite).to.equal false - should.exist(viewModel.email) - done() - - it 'build user stub id', (done) -> - @UserGetter.getUserOrUserStubById.yields(null, @userStub, true) - @UserMembershipViewModel.buildAsync ObjectId(), (error, viewModel) => - should.not.exist(error) - assertNotCalled(@UserMembershipViewModel.build) - expect(viewModel._id).to.equal @userStub._id - expect(viewModel.email).to.equal @userStub.email - expect(viewModel.invite).to.equal true - done() - - it 'build user id with error', (done) -> - @UserGetter.getUserOrUserStubById.yields(new Error('nope')) - userId = ObjectId() - @UserMembershipViewModel.buildAsync userId, (error, viewModel) => - should.not.exist(error) - assertNotCalled(@UserMembershipViewModel.build) - expect(viewModel._id).to.equal userId.toString() - should.not.exist(viewModel.email) - done() diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipsHandlerTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipsHandlerTests.coffee deleted file mode 100644 index 3017535378..0000000000 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipsHandlerTests.coffee +++ /dev/null @@ -1,40 +0,0 @@ -sinon = require('sinon') -assertCalledWith = sinon.assert.calledWith -ObjectId = require("../../../../app/js/infrastructure/mongojs").ObjectId -modulePath = "../../../../app/js/Features/UserMembership/UserMembershipsHandler" -SandboxedModule = require("sandboxed-module") - -describe 'UserMembershipsHandler', -> - beforeEach -> - @user = _id: ObjectId() - - @Institution = - updateMany: sinon.stub().yields(null) - @Subscription = - updateMany: sinon.stub().yields(null) - @Publisher = - updateMany: sinon.stub().yields(null) - @UserMembershipsHandler = SandboxedModule.require modulePath, requires: - '../../models/Institution': Institution: @Institution - '../../models/Subscription': Subscription: @Subscription - '../../models/Publisher': Publisher: @Publisher - - describe 'remove user', -> - it 'remove user from all entities', (done) -> - @UserMembershipsHandler.removeUserFromAllEntities @user._id, (error) => - assertCalledWith( - @Institution.updateMany, - {}, - { "$pull": { managerIds: @user._id } } - ) - assertCalledWith( - @Subscription.updateMany, - {}, - { "$pull": { manager_ids: @user._id } } - ) - assertCalledWith( - @Publisher.updateMany, - {}, - { "$pull": { managerIds: @user._id } } - ) - done() diff --git a/services/web/test/unit/coffee/helpers/MockClient.coffee b/services/web/test/unit/coffee/helpers/MockClient.coffee deleted file mode 100644 index 82c3c02b19..0000000000 --- a/services/web/test/unit/coffee/helpers/MockClient.coffee +++ /dev/null @@ -1,17 +0,0 @@ -sinon = require('sinon') - -idCounter = 0 - -module.exports = class MockClient - constructor: () -> - @attributes = {} - @join = sinon.stub() - @emit = sinon.stub() - @disconnect = sinon.stub() - @id = idCounter++ - set : (key, value, callback) -> - @attributes[key] = value - callback() if callback? - get : (key, callback) -> - callback null, @attributes[key] - disconnect: () -> diff --git a/services/web/test/unit/coffee/helpers/MockRequest.coffee b/services/web/test/unit/coffee/helpers/MockRequest.coffee deleted file mode 100644 index aea1b8deda..0000000000 --- a/services/web/test/unit/coffee/helpers/MockRequest.coffee +++ /dev/null @@ -1,16 +0,0 @@ -class MockRequest - param: (param) -> @params[param] - session: - destroy:-> - - params: {} - query: {} - body: {} - _parsedUrl:{} - i18n: - translate: (str)-> str - route: - path: '' - -module.exports = MockRequest - diff --git a/services/web/test/unit/coffee/helpers/MockResponse.coffee b/services/web/test/unit/coffee/helpers/MockResponse.coffee deleted file mode 100644 index 89c0b9f580..0000000000 --- a/services/web/test/unit/coffee/helpers/MockResponse.coffee +++ /dev/null @@ -1,88 +0,0 @@ -sinon = require "sinon" - -class MockResponse - constructor: -> - @rendered = false - @redirected = false - @returned = false - @headers = {} - - render: (template, variables) -> - @success = true - @rendered = true - @returned = true - @renderedTemplate = template - @renderedVariables = variables - @callback() if @callback? - - redirect: (url) -> - @success = true - @redirected = true - @returned = true - @redirectedTo = url - @callback() if @callback? - - sendStatus: (status) -> - if arguments.length < 2 - if typeof status != "number" - body = status - status = 200 - @statusCode = status - @returned = true - if 200 <= status < 300 - @success = true - else - @success = false - @callback() if @callback? - - send: (status, body) -> - if arguments.length < 2 - if typeof status != "number" - body = status - status = 200 - @statusCode = status - @returned = true - if 200 <= status < 300 - @success = true - else - @success = false - @body = body if body - @callback() if @callback? - - json: (status, body) -> - if arguments.length < 2 - if typeof status != "number" - body = status - status = @statusCode || 200 - @statusCode = status - @returned = true - @type = 'application/json' - if 200 <= status < 300 - @success = true - else - @success = false - @body = JSON.stringify(body) if body - @callback() if @callback? - - status: (status)-> - @statusCode = status - return @ - - - setHeader: (header, value) -> - @headers[header] = value - - setContentDisposition: sinon.stub() - - setTimeout: (@timout)-> - - header: sinon.stub() - - contentType: sinon.stub() - - end: (data, encoding) -> - @callback() if @callback - - type: (type) -> @type = type - -module.exports = MockResponse diff --git a/services/web/test/unit/coffee/infrastructure/CsrfTests.coffee b/services/web/test/unit/coffee/infrastructure/CsrfTests.coffee deleted file mode 100644 index 610b278ad5..0000000000 --- a/services/web/test/unit/coffee/infrastructure/CsrfTests.coffee +++ /dev/null @@ -1,114 +0,0 @@ -assert = require("chai").assert -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/infrastructure/Csrf.js" -SandboxedModule = require('sandboxed-module') - -describe "Csrf", -> - - beforeEach -> - @csurf_csrf = sinon.stub().callsArgWith(2, @err = {code: 'EBADCSRFTOKEN'}) - @Csrf = SandboxedModule.require modulePath, requires: - csurf: sinon.stub().returns(@csurf_csrf) - @csrf = new @Csrf() - @next = sinon.stub() - @path = '/foo/bar' - @req = - path: @path - method: 'POST' - @res = {} - - describe 'the middleware', -> - describe 'when there are no excluded routes', -> - it 'passes the csrf error on', -> - @csrf.middleware @req, @res, @next - expect(@next.calledWith(@err)).to.equal true - - describe 'when the route is excluded', -> - it 'does not pass the csrf error on', -> - @csrf.disableDefaultCsrfProtection(@path, 'POST') - @csrf.middleware @req, @res, @next - expect(@next.calledWith(@err)).to.equal false - - describe 'when there is a partial route match', -> - it 'passes the csrf error on when the match is too short', -> - @csrf.disableDefaultCsrfProtection('/foo', 'POST') - @csrf.middleware @req, @res, @next - expect(@next.calledWith(@err)).to.equal true - - it 'passes the csrf error on when the match is too long', -> - @csrf.disableDefaultCsrfProtection('/foo/bar/baz', 'POST') - @csrf.middleware @req, @res, @next - expect(@next.calledWith(@err)).to.equal true - - describe 'when there are multiple exclusions', -> - it 'does not pass the csrf error on when the match is present', -> - @csrf.disableDefaultCsrfProtection(@path, 'POST') - @csrf.disableDefaultCsrfProtection('/test', 'POST') - @csrf.disableDefaultCsrfProtection('/a/b/c', 'POST') - @csrf.middleware @req, @res, @next - expect(@next.calledWith(@err)).to.equal false - - it 'passes the csrf error on when the match is not present', -> - @csrf.disableDefaultCsrfProtection('/url', 'POST') - @csrf.disableDefaultCsrfProtection('/test', 'POST') - @csrf.disableDefaultCsrfProtection('/a/b/c', 'POST') - @csrf.middleware @req, @res, @next - expect(@next.calledWith(@err)).to.equal true - - describe 'when the method does not match', -> - it 'passes the csrf error on', -> - @csrf.disableDefaultCsrfProtection(@path, 'POST') - @req.method = 'GET' - @csrf.middleware @req, @res, @next - expect(@next.calledWith(@err)).to.equal true - - describe 'when the route is excluded, but the error is not a bad-csrf-token error', -> - it 'passes the error on', -> - @Csrf = SandboxedModule.require modulePath, requires: - csurf: @csurf = sinon.stub().returns(@csurf_csrf = sinon.stub().callsArgWith(2, err = {code: 'EOTHER'})) - @csrf = new @Csrf() - @csrf.disableDefaultCsrfProtection(@path, 'POST') - @csrf.middleware @req, @res, @next - expect(@next.calledWith(err)).to.equal true - expect(@next.calledWith(@err)).to.equal false - - describe 'validateRequest', -> - describe 'when the request is invalid', -> - it 'calls the callback with `false`', -> - @cb = sinon.stub() - @Csrf.validateRequest(@req, @cb) - expect(@cb.calledWith(false)).to.equal true - - describe 'when the request is valid', -> - it 'calls the callback with `true`', -> - @Csrf = SandboxedModule.require modulePath, requires: - csurf: @csurf = sinon.stub().returns(@csurf_csrf = sinon.stub().callsArg(2)) - @cb = sinon.stub() - @Csrf.validateRequest(@req, @cb) - expect(@cb.calledWith(true)).to.equal true - - describe 'validateToken', -> - describe 'when the request is invalid', -> - it 'calls the callback with `false`', -> - @cb = sinon.stub() - @Csrf.validateToken('token', {}, @cb) - expect(@cb.calledWith(false)).to.equal true - - describe 'when the request is valid', -> - it 'calls the callback with `true`', -> - @Csrf = SandboxedModule.require modulePath, requires: - csurf: @csurf = sinon.stub().returns(@csurf_csrf = sinon.stub().callsArg(2)) - @cb = sinon.stub() - @Csrf.validateToken('goodtoken', {}, @cb) - expect(@cb.calledWith(true)).to.equal true - - describe 'when there is no token', -> - it 'calls the callback with `false`', -> - @Csrf = SandboxedModule.require modulePath, requires: - csurf: @csurf = sinon.stub().returns(@csurf_csrf = sinon.stub().callsArg(2)) - @cb = sinon.stub() - @Csrf.validateToken(null, {}, @cb) - expect(@cb.calledWith(false)).to.equal true diff --git a/services/web/test/unit/coffee/infrastructure/GeoIpLookupTests.coffee b/services/web/test/unit/coffee/infrastructure/GeoIpLookupTests.coffee deleted file mode 100644 index 8f8f559d09..0000000000 --- a/services/web/test/unit/coffee/infrastructure/GeoIpLookupTests.coffee +++ /dev/null @@ -1,105 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/infrastructure/GeoIpLookup" -expect = require("chai").expect - -describe "GeoIpLookup", -> - - beforeEach -> - - @settings = - apis: - geoIpLookup: - url:"http://lookup.com" - @request = - get: sinon.stub() - @GeoIpLookup = SandboxedModule.require modulePath, requires: - "request": @request - "settings-sharelatex":@settings - "logger-sharelatex": - log:-> - err:-> - @ipAddress = "123.456.789.123" - - @stubbedResponse = - "ip":@ipAddress - "country_code":"GB" - "country_name":"United Kingdom" - "region_code":"H9" - "region_name":"London, City of" - "city":"London" - "zipcode":"SE16" - "latitude":51.0 - "longitude":-0.0493 - "metro_code":"" - "area_code":"" - - describe "getDetails", -> - beforeEach -> - @request.get.callsArgWith(1, null, null, @stubbedResponse) - - it "should request the details using the ip", (done)-> - @GeoIpLookup.getDetails @ipAddress, (err)=> - @request.get.calledWith({url:@settings.apis.geoIpLookup.url+"/"+@ipAddress, timeout:1000, json:true}).should.equal true - done() - - it "should return the ip details", (done)-> - @GeoIpLookup.getDetails @ipAddress, (err, returnedDetails)=> - assert.deepEqual returnedDetails, @stubbedResponse - done() - - it "should take the first ip in the string", (done)-> - @GeoIpLookup.getDetails " #{@ipAddress} 456.312.452.102 432.433.888.234", (err)=> - @request.get.calledWith({url:@settings.apis.geoIpLookup.url+"/"+@ipAddress, timeout:1000, json:true}).should.equal true - done() - - describe "getCurrencyCode", -> - - it "should return GBP for GB country", (done)-> - @GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, null, @stubbedResponse) - @GeoIpLookup.getCurrencyCode @ipAddress, (err, currencyCode)-> - currencyCode.should.equal "GBP" - done() - - it "should return GBP for gb country", (done)-> - @stubbedResponse.country_code = "gb" - @GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, null, @stubbedResponse) - @GeoIpLookup.getCurrencyCode @ipAddress, (err, currencyCode)-> - currencyCode.should.equal "GBP" - done() - - it "should return USD for US", (done)-> - @stubbedResponse.country_code = "US" - @GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, null, @stubbedResponse) - @GeoIpLookup.getCurrencyCode @ipAddress, (err, currencyCode)-> - currencyCode.should.equal "USD" - done() - - it "should return EUR for DE", (done)-> - @stubbedResponse.country_code = "DE" - @GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, null, @stubbedResponse) - @GeoIpLookup.getCurrencyCode @ipAddress, (err, currencyCode)-> - currencyCode.should.equal "EUR" - done() - - it "should default to USD if there is an error", (done)-> - @GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, "problem") - @GeoIpLookup.getCurrencyCode @ipAddress, (err, currencyCode)-> - currencyCode.should.equal "USD" - done() - - it "should default to USD if there are no details", (done)-> - @GeoIpLookup.getDetails = sinon.stub().callsArgWith(1) - @GeoIpLookup.getCurrencyCode @ipAddress, (err, currencyCode)-> - currencyCode.should.equal "USD" - done() - - it "should default to USD if there is no match for their country", (done)-> - @stubbedResponse.country_code = "Non existant" - @GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, null, @stubbedResponse) - @GeoIpLookup.getCurrencyCode @ipAddress, (err, currencyCode)-> - currencyCode.should.equal "USD" - done() diff --git a/services/web/test/unit/coffee/infrastructure/LockManager/ReleasingTheLock.coffee b/services/web/test/unit/coffee/infrastructure/LockManager/ReleasingTheLock.coffee deleted file mode 100644 index 76747defbb..0000000000 --- a/services/web/test/unit/coffee/infrastructure/LockManager/ReleasingTheLock.coffee +++ /dev/null @@ -1,27 +0,0 @@ -sinon = require('sinon') -assert = require('assert') -path = require('path') -modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' -lockKey = "lock:web:{#{5678}}" -lockValue = "123456" -SandboxedModule = require('sandboxed-module') - -describe 'LockManager - releasing the lock', ()-> - - deleteStub = sinon.stub().callsArgWith(4) - mocks = - "logger-sharelatex": log:-> - - "./RedisWrapper": - client: ()-> - auth:-> - eval:deleteStub - - LockManager = SandboxedModule.require(modulePath, requires: mocks) - LockManager.unlockScript = "this is the unlock script" - - it 'should put a all data into memory', (done)-> - LockManager._releaseLock lockKey, lockValue, -> - deleteStub.calledWith(LockManager.unlockScript, 1, lockKey, lockValue).should.equal true - done() - diff --git a/services/web/test/unit/coffee/infrastructure/LockManager/getLockTests.coffee b/services/web/test/unit/coffee/infrastructure/LockManager/getLockTests.coffee deleted file mode 100644 index 138ea9df14..0000000000 --- a/services/web/test/unit/coffee/infrastructure/LockManager/getLockTests.coffee +++ /dev/null @@ -1,110 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -path = require('path') -modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' -SandboxedModule = require('sandboxed-module') - -describe 'LockManager - getting the lock', -> - beforeEach -> - @LockManager = SandboxedModule.require modulePath, requires: - "logger-sharelatex": log:-> - "./RedisWrapper": - client: ()-> - auth:-> - "settings-sharelatex":{redis:{}} - "metrics-sharelatex": - inc:-> - gauge:-> - - @callback = sinon.stub() - @key = "lock:web:lockName:project-id}" - @namespace = 'lockName' - - describe "when the lock is not set", -> - beforeEach (done) -> - @LockManager._tryLock = sinon.stub().yields(null, true) - @LockManager._getLock @key, @namespace, (args...) => - @callback(args...) - done() - - it "should try to get the lock", -> - @LockManager._tryLock - .calledWith(@key, @namespace) - .should.equal true - - it "should only need to try once", -> - @LockManager._tryLock.callCount.should.equal 1 - - it "should return the callback", -> - @callback.calledWith(null).should.equal true - - describe "when the lock is initially set", -> - beforeEach (done) -> - startTime = Date.now() - tries = 0 - @LockManager.LOCK_TEST_INTERVAL = 5 - @LockManager._tryLock = (key, namespace, callback = (error, isFree) ->) -> - if (Date.now() - startTime < 20) or (tries < 2) - tries = tries + 1 - callback null, false - else - callback null, true - sinon.spy @LockManager, "_tryLock" - - @LockManager._getLock @key, @namespace, (args...) => - @callback(args...) - done() - - it "should call tryLock multiple times until free", -> - (@LockManager._tryLock.callCount > 1).should.equal true - - it "should return the callback", -> - @callback.calledWith(null).should.equal true - - describe "when the lock times out", -> - beforeEach (done) -> - time = Date.now() - @LockManager.MAX_LOCK_WAIT_TIME = 5 - @LockManager._tryLock = sinon.stub().yields(null, false) - @LockManager._getLock @key, @namespace, (args...) => - @callback(args...) - done() - - it "should return the callback with an error", -> - @callback.calledWith(new Error("timeout")).should.equal true - - describe "when there are multiple requests for the same lock", -> - beforeEach (done) -> - locked = false - @results = [] - @LockManager.LOCK_TEST_INTERVAL = 1 - @LockManager._tryLock = (key, namespace, callback = (error, gotLock, lockValue) ->) -> - if locked - callback null, false - else - locked = true # simulate getting the lock - callback null, true - # Start ten lock requests in order at 1ms 2ms 3ms... - # with them randomly holding the lock for 0-100ms. - # Use predefined values for the random delay to make the test - # deterministic. - randomDelays = [52, 45, 41, 84, 60, 81, 31, 46, 9, 43 ] - startTime = 0 - for randomDelay, i in randomDelays - do (randomDelay, i) => - startTime += 1 - setTimeout () => - # changing the next line to the old method of LockManager._getLockByPolling - # should give results in a random order and cause the test to fail. - @LockManager._getLock @key, @namespace, (args...) => - setTimeout () -> - locked = false # release the lock after a random amount of time - , randomDelay - @results.push i - if @results.length is 10 - done() - , startTime - - it "should process the requests in order", -> - @results.should.deep.equal [0,1,2,3,4,5,6,7,8,9] diff --git a/services/web/test/unit/coffee/infrastructure/LockManager/tryLockTests.coffee b/services/web/test/unit/coffee/infrastructure/LockManager/tryLockTests.coffee deleted file mode 100644 index 9988b8583b..0000000000 --- a/services/web/test/unit/coffee/infrastructure/LockManager/tryLockTests.coffee +++ /dev/null @@ -1,42 +0,0 @@ -sinon = require('sinon') -chai = require('chai') -should = chai.should() -path = require('path') -modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' -SandboxedModule = require('sandboxed-module') - -describe 'LockManager - trying the lock', -> - beforeEach -> - @LockManager = SandboxedModule.require modulePath, requires: - "logger-sharelatex": log:-> - "./RedisWrapper": - client: () => - auth:-> - set: @set = sinon.stub() - "settings-sharelatex":{redis:{}} - "metrics-sharelatex": inc:-> - @callback = sinon.stub() - @key = "lock:web:lockName:project-id}" - @namespace = "lockName" - - describe "when the lock is not set", -> - beforeEach -> - @set.callsArgWith(5, null, "OK") - @LockManager.randomLock = sinon.stub().returns("random-lock-value") - @LockManager._tryLock @key, @namespace, @callback - - it "should set the lock key with an expiry if it is not set", -> - @set.calledWith(@key, "random-lock-value", "EX", 30, "NX") - .should.equal true - - it "should return the callback with true", -> - @callback.calledWith(null, true).should.equal true - - describe "when the lock is already set", -> - beforeEach -> - @set.callsArgWith(5, null, null) - @LockManager._tryLock @key, @namespace, @callback - - it "should return the callback with false", -> - @callback.calledWith(null, false).should.equal true - diff --git a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee b/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee deleted file mode 100644 index 9a1d9ea17c..0000000000 --- a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee +++ /dev/null @@ -1,149 +0,0 @@ -sinon = require('sinon') -assertCalledWith = sinon.assert.calledWith -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = '../../../../app/js/infrastructure/ProxyManager' -SandboxedModule = require('sandboxed-module') -MockRequest = require "../helpers/MockRequest" -MockResponse = require "../helpers/MockResponse" - -describe "ProxyManager", -> - before -> - @settings = proxyUrls: {} - @request = sinon.stub().returns( - on: ()-> - pipe: ()-> - ) - @proxyManager = SandboxedModule.require modulePath, requires: - 'settings-sharelatex': @settings - 'logger-sharelatex': log: -> - 'request': @request - @proxyPath = '/foo/bar' - @req = new MockRequest() - @res = new MockResponse() - @next = sinon.stub() - - describe 'apply', -> - it 'applies all paths', -> - @router = get: sinon.stub() - @settings.proxyUrls = - '/foo/bar': '' - '/foo/:id': '' - @proxyManager.apply(@router) - sinon.assert.calledTwice(@router.get) - assertCalledWith(@router.get, '/foo/bar') - assertCalledWith(@router.get, '/foo/:id') - - it 'applies methods other than get', -> - @router = - post: sinon.stub() - put: sinon.stub() - @settings.proxyUrls = - '/foo/bar': {options: {method: 'post'}} - '/foo/:id': {options: {method: 'put'}} - @proxyManager.apply(@router) - sinon.assert.calledOnce(@router.post) - sinon.assert.calledOnce(@router.put) - assertCalledWith(@router.post, '/foo/bar') - assertCalledWith(@router.put, '/foo/:id') - - describe 'createProxy', -> - beforeEach -> - @req.url = @proxyPath - @req.route.path = @proxyPath - @req.query = {} - @req.params = {} - @req.headers = {} - @settings.proxyUrls = {} - - afterEach -> - @next.reset() - @request.reset() - - it 'does not calls next when match', -> - target = '/' - @settings.proxyUrls[@proxyPath] = target - @proxyManager.createProxy(target)(@req, @res, @next) - sinon.assert.notCalled(@next) - sinon.assert.called(@request) - - it 'proxy full URL', -> - targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash' - @settings.proxyUrls[@proxyPath] = targetUrl - @proxyManager.createProxy(targetUrl)(@req) - assertCalledWith(@request, {url: targetUrl}) - - it 'overwrite query', -> - targetUrl = 'foo.bar/baz?query' - @req.query = { requestQuery: 'important' } - @settings.proxyUrls[@proxyPath] = targetUrl - @proxyManager.createProxy(targetUrl)(@req) - newTargetUrl = 'foo.bar/baz?requestQuery=important' - assertCalledWith(@request, {url: newTargetUrl}) - - it 'handles target objects', -> - target = { baseUrl: 'api.v1', path: '/pa/th'} - @settings.proxyUrls[@proxyPath] = target - @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, {url: 'api.v1/pa/th'}) - - it 'handles missing baseUrl', -> - target = { path: '/pa/th'} - @settings.proxyUrls[@proxyPath] = target - @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, {url: 'undefined/pa/th'}) - - it 'handles dynamic path', -> - target = baseUrl: 'api.v1', path: (params) -> "/resource/#{params.id}" - @settings.proxyUrls['/res/:id'] = target - @req.url = '/res/123' - @req.route.path = '/res/:id' - @req.params = id: 123 - @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, {url: 'api.v1/resource/123'}) - - it 'set arbitrary options on request', -> - target = baseUrl: 'api.v1', path: '/foo', options: foo: 'bar' - @req.url = '/foo' - @req.route.path = '/foo' - @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, - foo: 'bar' - url: 'api.v1/foo' - ) - - it 'passes cookies', -> - target = baseUrl: 'api.v1', path: '/foo' - @req.url = '/foo' - @req.route.path = '/foo' - @req.headers = cookie: 'cookie' - @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, - headers: Cookie: 'cookie' - url: 'api.v1/foo' - ) - - it 'passes body for post', -> - target = baseUrl: 'api.v1', path: '/foo', options: method: 'post' - @req.url = '/foo' - @req.route.path = '/foo' - @req.body = foo: 'bar' - @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, - form: foo: 'bar' - method: 'post' - url: 'api.v1/foo' - ) - - it 'passes body for put', -> - target = baseUrl: 'api.v1', path: '/foo', options: method: 'put' - @req.url = '/foo' - @req.route.path = '/foo' - @req.body = foo: 'bar' - @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, - form: foo: 'bar' - method: 'put' - url: 'api.v1/foo' - ) \ No newline at end of file diff --git a/services/web/test/unit/coffee/infrastructure/RateLimterTests.coffee b/services/web/test/unit/coffee/infrastructure/RateLimterTests.coffee deleted file mode 100644 index 7d0416ee86..0000000000 --- a/services/web/test/unit/coffee/infrastructure/RateLimterTests.coffee +++ /dev/null @@ -1,103 +0,0 @@ -assert = require("chai").assert -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/infrastructure/RateLimiter.js" -SandboxedModule = require('sandboxed-module') - -describe "RateLimiter", -> - - beforeEach -> - @settings = - redis: - web: - port:"1234" - host:"somewhere" - password: "password" - @rclient = - incr: sinon.stub() - get: sinon.stub() - expire: sinon.stub() - exec: sinon.stub() - @rclient.multi = sinon.stub().returns(@rclient) - @RedisWrapper = - client: sinon.stub().returns(@rclient) - - @endpointName = "compiles" - @subject = "some-project-id" - @timeInterval = 20 - @throttleLimit = 5 - - @requires = - "settings-sharelatex":@settings - "logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()} - "metrics-sharelatex" : @Metrics = {inc: sinon.stub()} - "./RedisWrapper": @RedisWrapper - - @details = - endpointName: @endpointName - subjectName: @subject - throttle: @throttleLimit - timeInterval: @timeInterval - @key = "RateLimiter:#{@endpointName}:{#{@subject}}" - - - - - describe 'when action is permitted', -> - - beforeEach -> - @requires["rolling-rate-limiter"] = (opts) => - return sinon.stub().callsArgWith(1, null, 0, 22) - @limiter = SandboxedModule.require modulePath, requires: @requires - - it 'should not produce and error', (done) -> - @limiter.addCount {}, (err, should) -> - expect(err).to.equal null - done() - - it 'should callback with true', (done) -> - @limiter.addCount {}, (err, should) -> - expect(should).to.equal true - done() - - it 'should not increment the metric', (done) -> - @limiter.addCount {endpointName: @endpointName}, (err, should) => - sinon.assert.notCalled(@Metrics.inc) - done() - - describe 'when action is not permitted', -> - - beforeEach -> - @requires["rolling-rate-limiter"] = (opts) => - return sinon.stub().callsArgWith(1, null, 4000, 0) - @limiter = SandboxedModule.require modulePath, requires: @requires - - it 'should not produce and error', (done) -> - @limiter.addCount {}, (err, should) -> - expect(err).to.equal null - done() - - it 'should callback with false', (done) -> - @limiter.addCount {}, (err, should) -> - expect(should).to.equal false - done() - - it 'should increment the metric', (done) -> - @limiter.addCount {endpointName: @endpointName}, (err, should) => - sinon.assert.calledWith(@Metrics.inc, "rate-limit-hit.#{@endpointName}", 1, {path: @endpointName}) - done() - - describe 'when limiter produces an error', -> - - beforeEach -> - @requires["rolling-rate-limiter"] = (opts) => - return sinon.stub().callsArgWith(1, new Error('woops')) - @limiter = SandboxedModule.require modulePath, requires: @requires - - it 'should produce and error', (done) -> - @limiter.addCount {}, (err, should) -> - expect(err).to.not.equal null - expect(err).to.be.instanceof Error - done() diff --git a/services/web/test/unit/coffee/infrastructure/RedisWrapperTests.coffee b/services/web/test/unit/coffee/infrastructure/RedisWrapperTests.coffee deleted file mode 100644 index 4837711f56..0000000000 --- a/services/web/test/unit/coffee/infrastructure/RedisWrapperTests.coffee +++ /dev/null @@ -1,36 +0,0 @@ -assert = require("chai").assert -sinon = require('sinon') -chai = require('chai') -should = chai.should() -expect = chai.expect -modulePath = "../../../../app/js/infrastructure/RedisWrapper.js" -SandboxedModule = require('sandboxed-module') - -describe 'RedisWrapper', -> - - beforeEach -> - @settings = { redis: {} } - @redis = - createClient: sinon.stub() - @RedisWrapper = SandboxedModule.require modulePath, requires: - 'settings-sharelatex': @settings - 'redis-sharelatex': @redis - - describe 'client', -> - it "should use the feature settings if present", -> - @settings.redis = - my_feature: - port:"23456" - host:"otherhost" - password: "banana" - @RedisWrapper.client("my_feature") - @redis.createClient.calledWith(@settings.redis.my_feature).should.equal true - - it "should use the web settings if feature not present", -> - @settings.redis = - web: - port:"43" - host:"otherhost" - password: "banana" - @RedisWrapper.client("my_feature") - @redis.createClient.calledWith(@settings.redis.web).should.equal true diff --git a/services/web/test/unit/src/Analytics/AnalyticsControllerTests.js b/services/web/test/unit/src/Analytics/AnalyticsControllerTests.js new file mode 100644 index 0000000000..dec9816bd3 --- /dev/null +++ b/services/web/test/unit/src/Analytics/AnalyticsControllerTests.js @@ -0,0 +1,135 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Analytics/AnalyticsController' +) +const sinon = require('sinon') +const { expect } = require('chai') + +describe('AnalyticsController', function() { + beforeEach(function() { + this.AuthenticationController = { getLoggedInUserId: sinon.stub() } + + this.AnalyticsManager = { + updateEditingSession: sinon.stub().callsArgWith(3), + recordEvent: sinon.stub().callsArgWith(3) + } + + this.InstitutionsAPI = { + getInstitutionLicences: sinon.stub().callsArgWith(4) + } + + this.controller = SandboxedModule.require(modulePath, { + requires: { + './AnalyticsManager': this.AnalyticsManager, + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../Institutions/InstitutionsAPI': this.InstitutionsAPI, + 'logger-sharelatex': { + log() {} + }, + '../../infrastructure/GeoIpLookup': (this.GeoIpLookup = { + getDetails: sinon.stub() + }) + } + }) + + return (this.res = { send() {} }) + }) + + describe('updateEditingSession', function() { + beforeEach(function() { + this.req = { + params: { + projectId: 'a project id' + } + } + return (this.GeoIpLookup.getDetails = sinon + .stub() + .callsArgWith(1, null, { country_code: 'XY' })) + }) + + return it('delegates to the AnalyticsManager', function(done) { + this.AuthenticationController.getLoggedInUserId.returns('1234') + this.controller.updateEditingSession(this.req, this.res) + + this.AnalyticsManager.updateEditingSession + .calledWith('1234', 'a project id', 'XY') + .should.equal(true) + return done() + }) + }) + + describe('recordEvent', function() { + beforeEach(function() { + return (this.req = { + params: { + event: 'i_did_something' + }, + body: 'stuff', + sessionID: 'sessionIDHere', + session: {} + }) + }) + + it('should use the user_id', function(done) { + this.AuthenticationController.getLoggedInUserId.returns('1234') + this.controller.recordEvent(this.req, this.res) + this.AnalyticsManager.recordEvent + .calledWith('1234', this.req.params['event'], this.req.body) + .should.equal(true) + return done() + }) + + return it('should use the session id', function(done) { + this.controller.recordEvent(this.req, this.res) + this.AnalyticsManager.recordEvent + .calledWith(this.req.sessionID, this.req.params['event'], this.req.body) + .should.equal(true) + return done() + }) + }) + + return describe('licences', function() { + beforeEach(function() { + return (this.req = { + query: { + resource_id: 1, + start_date: '1514764800', + end_date: '1530662400', + resource_type: 'institution' + }, + sessionID: 'sessionIDHere', + session: {} + }) + }) + + return it('should trigger institutions api to fetch licences graph data', function(done) { + this.controller.licences(this.req, this.res) + this.InstitutionsAPI.getInstitutionLicences + .calledWith( + this.req.query['resource_id'], + this.req.query['start_date'], + this.req.query['end_date'], + this.req.query['lag'] + ) + .should.equal(true) + return done() + }) + }) +}) diff --git a/services/web/test/unit/src/Announcement/AnnouncementsHandlerTests.js b/services/web/test/unit/src/Announcement/AnnouncementsHandlerTests.js new file mode 100644 index 0000000000..6b5c1bb5c6 --- /dev/null +++ b/services/web/test/unit/src/Announcement/AnnouncementsHandlerTests.js @@ -0,0 +1,247 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Announcements/AnnouncementsHandler' +) +const sinon = require('sinon') +const { expect } = require('chai') + +describe('AnnouncementsHandler', function() { + beforeEach(function() { + this.user = { + _id: '3c6afe000000000000000000', // 2002-02-14T00:00:00.000Z + email: 'someone@gmail.com' + } + this.AnalyticsManager = { getLastOccurrence: sinon.stub() } + this.BlogHandler = { getLatestAnnouncements: sinon.stub() } + this.settings = {} + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + '../Analytics/AnalyticsManager': this.AnalyticsManager, + '../Blog/BlogHandler': this.BlogHandler, + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {} + } + } + })) + }) + + return describe('getUnreadAnnouncements', function() { + beforeEach(function() { + this.stubbedAnnouncements = [ + { + date: new Date(1478836800000), + id: '/2016/11/01/introducting-latex-code-checker' + }, + { + date: new Date(1308369600000), + id: '/2013/08/02/thesis-series-pt1' + }, + { + date: new Date(1108369600000), + id: '/2005/08/04/somethingelse' + }, + { + date: new Date(1208369600000), + id: '/2008/04/12/title-date-irrelivant' + } + ] + return this.BlogHandler.getLatestAnnouncements.callsArgWith( + 0, + null, + this.stubbedAnnouncements + ) + }) + + it('should mark all announcements as read is false', function(done) { + this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) + return this.handler.getUnreadAnnouncements( + this.user, + (err, announcements) => { + announcements[0].read.should.equal(false) + announcements[1].read.should.equal(false) + announcements[2].read.should.equal(false) + announcements[3].read.should.equal(false) + return done() + } + ) + }) + + it('should should be sorted again to ensure correct order', function(done) { + this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) + return this.handler.getUnreadAnnouncements( + this.user, + (err, announcements) => { + announcements[3].should.equal(this.stubbedAnnouncements[2]) + announcements[2].should.equal(this.stubbedAnnouncements[3]) + announcements[1].should.equal(this.stubbedAnnouncements[1]) + announcements[0].should.equal(this.stubbedAnnouncements[0]) + return done() + } + ) + }) + + it('should return older ones marked as read as well', function(done) { + this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, { + segmentation: { blogPostId: '/2008/04/12/title-date-irrelivant' } + }) + return this.handler.getUnreadAnnouncements( + this.user, + (err, announcements) => { + announcements[0].id.should.equal(this.stubbedAnnouncements[0].id) + announcements[0].read.should.equal(false) + + announcements[1].id.should.equal(this.stubbedAnnouncements[1].id) + announcements[1].read.should.equal(false) + + announcements[2].id.should.equal(this.stubbedAnnouncements[3].id) + announcements[2].read.should.equal(true) + + announcements[3].id.should.equal(this.stubbedAnnouncements[2].id) + announcements[3].read.should.equal(true) + + return done() + } + ) + }) + + it('should return all of them marked as read', function(done) { + this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, { + segmentation: { + blogPostId: '/2016/11/01/introducting-latex-code-checker' + } + }) + return this.handler.getUnreadAnnouncements( + this.user, + (err, announcements) => { + announcements[0].read.should.equal(true) + announcements[1].read.should.equal(true) + announcements[2].read.should.equal(true) + announcements[3].read.should.equal(true) + return done() + } + ) + }) + + it('should return posts older than signup date as read', function(done) { + this.stubbedAnnouncements.push({ + date: new Date(978836800000), + id: '/2001/04/12/title-date-irrelivant' + }) + this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) + return this.handler.getUnreadAnnouncements( + this.user, + (err, announcements) => { + announcements[0].read.should.equal(false) + announcements[1].read.should.equal(false) + announcements[2].read.should.equal(false) + announcements[3].read.should.equal(false) + announcements[4].read.should.equal(true) + announcements[4].id.should.equal('/2001/04/12/title-date-irrelivant') + return done() + } + ) + }) + + describe('with custom domain announcements', function() { + beforeEach(function() { + this.stubbedDomainSpecificAnn = [ + { + domains: ['gmail.com', 'yahoo.edu'], + title: 'some message', + excerpt: 'read this', + url: 'http://www.sharelatex.com/i/somewhere', + id: 'iaaa', + date: new Date(1308369600000).toString() + } + ] + + return (this.handler._domainSpecificAnnouncements = sinon + .stub() + .returns(this.stubbedDomainSpecificAnn)) + }) + + return it('should insert the domain specific in the correct place', function(done) { + this.AnalyticsManager.getLastOccurrence.callsArgWith(2, null, []) + return this.handler.getUnreadAnnouncements( + this.user, + (err, announcements) => { + announcements[4].should.equal(this.stubbedAnnouncements[2]) + announcements[3].should.equal(this.stubbedAnnouncements[3]) + announcements[2].should.equal(this.stubbedAnnouncements[1]) + announcements[1].should.equal(this.stubbedDomainSpecificAnn[0]) + announcements[0].should.equal(this.stubbedAnnouncements[0]) + return done() + } + ) + }) + }) + + return describe('_domainSpecificAnnouncements', function() { + beforeEach(function() { + return (this.settings.domainAnnouncements = [ + { + domains: ['gmail.com', 'yahoo.edu'], + title: 'some message', + excerpt: 'read this', + url: 'http://www.sharelatex.com/i/somewhere', + id: 'id1', + date: new Date(1308369600000).toString() + }, + { + domains: ['gmail.com', 'yahoo.edu'], + title: 'some message', + excerpt: 'read this', + url: 'http://www.sharelatex.com/i/somewhere', + date: new Date(1308369600000).toString() + }, + { + domains: ['gmail.com', 'yahoo.edu'], + title: 'some message', + excerpt: 'read this', + url: 'http://www.sharelatex.com/i/somewhere', + id: 'id3', + date: new Date(1308369600000).toString() + } + ]) + }) + + it("should filter announcments which don't have an id", function(done) { + const result = this.handler._domainSpecificAnnouncements( + 'someone@gmail.com' + ) + result.length.should.equal(2) + result[0].id.should.equal('id1') + result[1].id.should.equal('id3') + return done() + }) + + return it('should match on domain', function(done) { + this.settings.domainAnnouncements[2].domains = ['yahoo.com'] + const result = this.handler._domainSpecificAnnouncements( + 'someone@gmail.com' + ) + result.length.should.equal(1) + result[0].id.should.equal('id1') + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js new file mode 100644 index 0000000000..851be0a921 --- /dev/null +++ b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js @@ -0,0 +1,1298 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Authentication/AuthenticationController.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const tk = require('timekeeper') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const { ObjectId } = require('mongojs') + +describe('AuthenticationController', function() { + beforeEach(function() { + tk.freeze(Date.now()) + this.UserModel = { findOne: sinon.stub() } + this.AuthenticationController = SandboxedModule.require(modulePath, { + requires: { + './AuthenticationManager': (this.AuthenticationManager = {}), + '../User/UserUpdater': (this.UserUpdater = { + updateUser: sinon.stub() + }), + 'metrics-sharelatex': (this.Metrics = { inc: sinon.stub() }), + '../Security/LoginRateLimiter': (this.LoginRateLimiter = { + processLoginRequest: sinon.stub(), + recordSuccessfulLogin: sinon.stub() + }), + '../User/UserHandler': (this.UserHandler = { + setupLoginData: sinon.stub() + }), + '../Analytics/AnalyticsManager': (this.AnalyticsManager = { + recordEvent: sinon.stub() + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err: sinon.stub() + }), + 'settings-sharelatex': { siteUrl: 'http://www.foo.bar' }, + passport: (this.passport = { + authenticate: sinon.stub().returns(sinon.stub()) + }), + '../User/UserSessionsManager': (this.UserSessionsManager = { + trackSession: sinon.stub(), + untrackSession: sinon.stub(), + revokeAllUserSessions: sinon.stub().callsArgWith(1, null) + }), + '../../infrastructure/Modules': (this.Modules = { + hooks: { fire: sinon.stub().callsArgWith(2, null, []) } + }), + '../SudoMode/SudoModeHandler': (this.SudoModeHandler = { + activateSudoMode: sinon.stub().callsArgWith(1, null) + }), + '../Notifications/NotificationsBuilder': (this.NotificationsBuilder = { + ipMatcherAffiliation: sinon.stub() + }), + '../V1/V1Api': (this.V1Api = { request: sinon.stub() }), + '../../models/User': { User: this.UserModel }, + '../../../../modules/oauth2-server/app/src/Oauth2Server': (this.Oauth2Server = { + Request: sinon.stub(), + Response: sinon.stub(), + server: { + authenticate: sinon.stub() + } + }) + } + }) + this.user = { + _id: ObjectId(), + email: (this.email = 'USER@example.com'), + first_name: 'bob', + last_name: 'brown', + referal_id: 1234, + isAdmin: false + } + this.password = 'banana' + this.req = new MockRequest() + this.res = new MockResponse() + return (this.callback = this.next = sinon.stub()) + }) + + afterEach(() => tk.reset()) + + describe('isUserLoggedIn', function() { + beforeEach(function() { + return (this.stub = sinon.stub( + this.AuthenticationController, + 'getLoggedInUserId' + )) + }) + + afterEach(function() { + return this.stub.restore() + }) + + return it('should do the right thing in all cases', function() { + this.AuthenticationController.getLoggedInUserId.returns('some_id') + expect(this.AuthenticationController.isUserLoggedIn(this.req)).to.equal( + true + ) + this.AuthenticationController.getLoggedInUserId.returns(null) + expect(this.AuthenticationController.isUserLoggedIn(this.req)).to.equal( + false + ) + this.AuthenticationController.getLoggedInUserId.returns(false) + expect(this.AuthenticationController.isUserLoggedIn(this.req)).to.equal( + false + ) + this.AuthenticationController.getLoggedInUserId.returns(undefined) + return expect( + this.AuthenticationController.isUserLoggedIn(this.req) + ).to.equal(false) + }) + }) + + describe('setInSessionUser', function() { + beforeEach(function() { + this.user = { + _id: 'id', + first_name: 'a', + last_name: 'b', + email: 'c' + } + this.req.session.passport = { user: this.user } + return (this.req.session.user = this.user) + }) + + return it('should update the right properties', function() { + this.AuthenticationController.setInSessionUser(this.req, { + first_name: 'new_first_name', + email: 'new_email' + }) + const expectedUser = { + _id: 'id', + first_name: 'new_first_name', + last_name: 'b', + email: 'new_email' + } + expect(this.req.session.passport.user).to.deep.equal(expectedUser) + return expect(this.req.session.user).to.deep.equal(expectedUser) + }) + }) + + describe('passportLogin', function() { + beforeEach(function() { + this.info = null + this.req.login = sinon.stub().callsArgWith(1, null) + this.res.json = sinon.stub() + this.req.session = this.session = { + passport: { user: this.user }, + postLoginRedirect: '/path/to/redir/to' + } + this.req.session.destroy = sinon.stub().callsArgWith(0, null) + this.req.session.save = sinon.stub().callsArgWith(0, null) + this.req.sessionStore = { generate: sinon.stub() } + this.AuthenticationController.finishLogin = sinon.stub() + this.passport.authenticate.callsArgWith(1, null, this.user, this.info) + return (this.err = new Error('woops')) + }) + + it('should call passport.authenticate', function() { + this.AuthenticationController.passportLogin(this.req, this.res, this.next) + return this.passport.authenticate.callCount.should.equal(1) + }) + + describe('when authenticate produces an error', function() { + beforeEach(function() { + return this.passport.authenticate.callsArgWith(1, this.err) + }) + + return it('should return next with an error', function() { + this.AuthenticationController.passportLogin( + this.req, + this.res, + this.next + ) + return this.next.calledWith(this.err).should.equal(true) + }) + }) + + describe('when authenticate produces a user', function() { + beforeEach(function() { + this.req.session.postLoginRedirect = 'some_redirect' + return this.passport.authenticate.callsArgWith( + 1, + null, + this.user, + this.info + ) + }) + + afterEach(function() { + return delete this.req.session.postLoginRedirect + }) + + return it('should call finishLogin', function() { + this.AuthenticationController.passportLogin( + this.req, + this.res, + this.next + ) + this.AuthenticationController.finishLogin.callCount.should.equal(1) + return this.AuthenticationController.finishLogin + .calledWith(this.user) + .should.equal(true) + }) + }) + + return describe('when authenticate does not produce a user', function() { + beforeEach(function() { + this.info = { text: 'a', type: 'b' } + return this.passport.authenticate.callsArgWith( + 1, + null, + false, + this.info + ) + }) + + it('should not call finishLogin', function() { + this.AuthenticationController.passportLogin( + this.req, + this.res, + this.next + ) + return this.AuthenticationController.finishLogin.callCount.should.equal( + 0 + ) + }) + + return it('should not send a json response with redirect', function() { + this.AuthenticationController.passportLogin( + this.req, + this.res, + this.next + ) + this.res.json.callCount.should.equal(1) + this.res.json.calledWith({ message: this.info }).should.equal(true) + return expect(this.res.json.lastCall.args[0].redir != null).to.equal( + false + ) + }) + }) + }) + + describe('afterLoginSessionSetup', function() { + beforeEach(function() { + this.req.login = sinon.stub().callsArgWith(1, null) + this.req.session = this.session = { passport: { user: this.user } } + this.req.session = { passport: { user: { _id: 'one' } } } + this.req.session.destroy = sinon.stub().callsArgWith(0, null) + this.req.session.save = sinon.stub().callsArgWith(0, null) + this.req.sessionStore = { generate: sinon.stub() } + this.UserSessionsManager.trackSession = sinon.stub() + return (this.call = callback => { + return this.AuthenticationController.afterLoginSessionSetup( + this.req, + this.user, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should call req.login', function(done) { + return this.call(err => { + this.req.login.callCount.should.equal(1) + return done() + }) + }) + + it('should call req.session.save', function(done) { + return this.call(err => { + this.req.session.save.callCount.should.equal(1) + return done() + }) + }) + + it('should call UserSessionsManager.trackSession', function(done) { + return this.call(err => { + this.UserSessionsManager.trackSession.callCount.should.equal(1) + return done() + }) + }) + + return describe('when req.session.save produces an error', function() { + beforeEach(function() { + return (this.req.session.save = sinon + .stub() + .callsArgWith(0, new Error('woops'))) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.oneOf([null, undefined]) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not call UserSessionsManager.trackSession', function(done) { + return this.call(err => { + this.UserSessionsManager.trackSession.callCount.should.equal(0) + return done() + }) + }) + }) + }) + + describe('getSessionUser', function() { + it('should get the user object from session', function() { + this.req.session = { + passport: { + user: { _id: 'one' } + } + } + const user = this.AuthenticationController.getSessionUser(this.req) + return expect(user).to.deep.equal({ _id: 'one' }) + }) + + return it('should work with legacy sessions', function() { + this.req.session = { user: { _id: 'one' } } + const user = this.AuthenticationController.getSessionUser(this.req) + return expect(user).to.deep.equal({ _id: 'one' }) + }) + }) + + describe('doPassportLogin', function() { + beforeEach(function() { + this.AuthenticationController._recordFailedLogin = sinon.stub() + this.AuthenticationController._recordSuccessfulLogin = sinon.stub() + this.Modules.hooks.fire = sinon.stub().callsArgWith(3, null, []) + // @AuthenticationController.establishUserSession = sinon.stub().callsArg(2) + this.req.body = { + email: this.email, + password: this.password, + session: { + postLoginRedirect: '/path/to/redir/to' + } + } + return (this.cb = sinon.stub()) + }) + + describe('when the preDoPassportLogin hooks produce an info object', function() { + beforeEach(function() { + return (this.Modules.hooks.fire = sinon + .stub() + .callsArgWith(3, null, [null, { redir: '/somewhere' }, null])) + }) + + return it('should stop early and call done with this info object', function(done) { + this.AuthenticationController.doPassportLogin( + this.req, + this.req.body.email, + this.req.body.password, + this.cb + ) + this.cb.callCount.should.equal(1) + this.cb + .calledWith(null, false, { redir: '/somewhere' }) + .should.equal(true) + this.LoginRateLimiter.processLoginRequest.callCount.should.equal(0) + return done() + }) + }) + + describe('when the users rate limit', function() { + beforeEach(function() { + return this.LoginRateLimiter.processLoginRequest.callsArgWith( + 1, + null, + false + ) + }) + + return it('should block the request if the limit has been exceeded', function(done) { + this.AuthenticationController.doPassportLogin( + this.req, + this.req.body.email, + this.req.body.password, + this.cb + ) + this.cb.callCount.should.equal(1) + this.cb.calledWith(null, null).should.equal(true) + return done() + }) + }) + + describe('when the user is authenticated', function() { + beforeEach(function() { + this.cb = sinon.stub() + this.LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true) + this.AuthenticationManager.authenticate = sinon + .stub() + .callsArgWith(2, null, this.user) + this.req.sessionID = Math.random() + return this.AuthenticationController.doPassportLogin( + this.req, + this.req.body.email, + this.req.body.password, + this.cb + ) + }) + + it('should attempt to authorise the user', function() { + return this.AuthenticationManager.authenticate + .calledWith({ email: this.email.toLowerCase() }, this.password) + .should.equal(true) + }) + + return it("should establish the user's session", function() { + return this.cb.calledWith(null, this.user).should.equal(true) + }) + }) + + describe('_loginAsyncHandlers', function() { + beforeEach(function() { + this.UserHandler.setupLoginData = sinon.stub() + this.LoginRateLimiter.recordSuccessfulLogin = sinon.stub() + this.AuthenticationController._recordSuccessfulLogin = sinon.stub() + this.AnalyticsManager.recordEvent = sinon.stub() + this.AnalyticsManager.identifyUser = sinon.stub() + return this.AuthenticationController._loginAsyncHandlers( + this.req, + this.user + ) + }) + + it('should call identifyUser', function() { + return this.AnalyticsManager.identifyUser + .calledWith(this.user._id, this.req.sessionID) + .should.equal(true) + }) + + it('should setup the user data in the background', function() { + return this.UserHandler.setupLoginData + .calledWith(this.user) + .should.equal(true) + }) + + it('should set res.session.justLoggedIn', function() { + return this.req.session.justLoggedIn.should.equal(true) + }) + + it('should record the successful login', function() { + return this.AuthenticationController._recordSuccessfulLogin + .calledWith(this.user._id) + .should.equal(true) + }) + + it('should tell the rate limiter that there was a success for that email', function() { + return this.LoginRateLimiter.recordSuccessfulLogin + .calledWith(this.user.email) + .should.equal(true) + }) + + it('should log the successful login', function() { + return this.logger.log + .calledWith( + { email: this.user.email, user_id: this.user._id.toString() }, + 'successful log in' + ) + .should.equal(true) + }) + + return it('should track the login event', function() { + return this.AnalyticsManager.recordEvent + .calledWith(this.user._id, 'user-logged-in') + .should.equal(true) + }) + }) + + return describe('when the user is not authenticated', function() { + beforeEach(function() { + this.LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true) + this.AuthenticationManager.authenticate = sinon + .stub() + .callsArgWith(2, null, null) + this.cb = sinon.stub() + return this.AuthenticationController.doPassportLogin( + this.req, + this.req.body.email, + this.req.body.password, + this.cb + ) + }) + + it('should not establish the login', function() { + this.cb.callCount.should.equal(1) + this.cb.calledWith(null, false) + // @res.body.should.exist + return expect(this.cb.lastCall.args[2]).to.contain.all.keys([ + 'text', + 'type' + ]) + }) + // message: + // text: 'Your email or password were incorrect. Please try again', + // type: 'error' + + it('should not setup the user data in the background', function() { + return this.UserHandler.setupLoginData.called.should.equal(false) + }) + + it('should record a failed login', function() { + return this.AuthenticationController._recordFailedLogin.called.should.equal( + true + ) + }) + + return it('should log the failed login', function() { + return this.logger.log + .calledWith({ email: this.email.toLowerCase() }, 'failed log in') + .should.equal(true) + }) + }) + }) + + describe('getLoggedInUserId', function() { + beforeEach(function() { + return (this.req = { session: {} }) + }) + + it('should return the user id from the session', function() { + this.user_id = '2134' + this.req.session.user = { _id: this.user_id } + const result = this.AuthenticationController.getLoggedInUserId(this.req) + return expect(result).to.equal(this.user_id) + }) + + it('should return user for passport session', function() { + this.user_id = '2134' + this.req.session = { + passport: { + user: { + _id: this.user_id + } + } + } + const result = this.AuthenticationController.getLoggedInUserId(this.req) + return expect(result).to.equal(this.user_id) + }) + + it('should return null if there is no user on the session', function() { + const result = this.AuthenticationController.getLoggedInUserId(this.req) + return expect(result).to.equal(null) + }) + + it('should return null if there is no session', function() { + this.req = {} + const result = this.AuthenticationController.getLoggedInUserId(this.req) + return expect(result).to.equal(null) + }) + + return it('should return null if there is no req', function() { + this.req = {} + const result = this.AuthenticationController.getLoggedInUserId(this.req) + return expect(result).to.equal(null) + }) + }) + + describe('requireLogin', function() { + beforeEach(function() { + this.user = { + _id: 'user-id-123', + email: 'user@sharelatex.com' + } + return (this.middleware = this.AuthenticationController.requireLogin()) + }) + + describe('when the user is logged in', function() { + beforeEach(function() { + this.req.session = { + user: (this.user = { + _id: 'user-id-123', + email: 'user@sharelatex.com' + }) + } + return this.middleware(this.req, this.res, this.next) + }) + + return it('should call the next method in the chain', function() { + return this.next.called.should.equal(true) + }) + }) + + return describe('when the user is not logged in', function() { + beforeEach(function() { + this.req.session = {} + this.AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() + this.req.query = {} + return this.middleware(this.req, this.res, this.next) + }) + + return it('should redirect to the register or login page', function() { + return this.AuthenticationController._redirectToLoginOrRegisterPage + .calledWith(this.req, this.res) + .should.equal(true) + }) + }) + }) + + describe('requireOauth', function() { + beforeEach(function() { + this.res.sendStatus = sinon.stub() + this.res.send = sinon.stub() + this.res.status = sinon.stub().returns(this.res) + this.res.sendStatus = sinon.stub() + return (this.middleware = this.AuthenticationController.requireOauth()) + }) + + describe('when Oauth2Server authenticates', function() { + beforeEach(function() { + this.token = { + accessToken: 'token', + user: 'user' + } + this.Oauth2Server.server.authenticate.yields(null, this.token) + return this.middleware(this.req, this.res, this.next) + }) + + it('should set oauth_token on request', function() { + return this.req.oauth_token.should.equal(this.token) + }) + + it('should set oauth on request', function() { + return this.req.oauth.access_token.should.equal(this.token.accessToken) + }) + + it('should set oauth_user on request', function() { + return this.req.oauth_user.should.equal('user') + }) + + return it('should call next', function() { + return this.next.should.have.been.calledOnce + }) + }) + + return describe('when Oauth2Server does not authenticate', function() { + beforeEach(function() { + return this.Oauth2Server.server.authenticate.yields({ code: 401 }) + }) + + describe('when token not provided', function() { + beforeEach(function() { + return this.middleware(this.req, this.res, this.next) + }) + + return it('should return 401 error', function() { + return this.res.sendStatus.should.have.been.calledWith(401) + }) + }) + + describe('when token provided', function() { + beforeEach(function() { + this.V1Api.request = sinon.stub().yields('error', {}, {}) + this.req.token = 'foo' + return this.middleware(this.req, this.res, this.next) + }) + + return it('should make request to v1 api with token', function() { + return this.V1Api.request.should.have.been.calledWith({ + expectedStatusCodes: [401], + json: { + token: 'foo' + }, + method: 'POST', + uri: '/api/v1/sharelatex/oauth_authorize' + }) + }) + }) + + describe('when v1 api returns error', function() { + beforeEach(function() { + this.V1Api.request = sinon.stub().yields('error', {}, {}) + this.req.token = 'foo' + return this.middleware(this.req, this.res, this.next) + }) + + return it('should return status', function() { + return this.next.should.have.been.calledWith('error') + }) + }) + + describe('when v1 api status code is not 200', function() { + beforeEach(function() { + this.V1Api.request = sinon + .stub() + .yields(null, { statusCode: 401 }, {}) + this.req.token = 'foo' + return this.middleware(this.req, this.res, this.next) + }) + + return it('should return status', function() { + return this.res.status.should.have.been.calledWith(401) + }) + }) + + return describe('when v1 api returns authorized profile and access token', function() { + beforeEach(function() { + this.oauth_authorize = { + access_token: 'access_token', + user_profile: { + id: 'overleaf-id' + } + } + this.V1Api.request = sinon + .stub() + .yields(null, { statusCode: 200 }, this.oauth_authorize) + return (this.req.token = 'foo') + }) + + describe('in all cases', function() { + beforeEach(function() { + return this.middleware(this.req, this.res, this.next) + }) + + return it('should find user', function() { + return this.UserModel.findOne.should.have.been.calledWithMatch({ + 'overleaf.id': 'overleaf-id' + }) + }) + }) + + describe('when user find returns error', function() { + beforeEach(function() { + this.UserModel.findOne = sinon.stub().yields('error') + return this.middleware(this.req, this.res, this.next) + }) + + return it('should return error', function() { + return this.next.should.have.been.calledWith('error') + }) + }) + + describe('when user is not found', function() { + beforeEach(function() { + this.UserModel.findOne = sinon.stub().yields(null, null) + return this.middleware(this.req, this.res, this.next) + }) + + return it('should return unauthorized', function() { + return this.res.status.should.have.been.calledWith(401) + }) + }) + + return describe('when user is found', function() { + beforeEach(function() { + this.UserModel.findOne = sinon.stub().yields(null, 'user') + return this.middleware(this.req, this.res, this.next) + }) + + it('should add user to request', function() { + return this.req.oauth_user.should.equal('user') + }) + + return it('should add access_token to request', function() { + return this.req.oauth.access_token.should.equal('access_token') + }) + }) + }) + }) + }) + + describe('requireGlobalLogin', function() { + beforeEach(function() { + this.req.headers = {} + this.AuthenticationController.httpAuth = sinon.stub() + return (this.setRedirect = sinon.spy( + this.AuthenticationController, + 'setRedirectInSession' + )) + }) + + afterEach(function() { + return this.setRedirect.restore() + }) + + describe('with white listed url', function() { + beforeEach(function() { + this.AuthenticationController.addEndpointToLoginWhitelist('/login') + this.req._parsedUrl.pathname = '/login' + return this.AuthenticationController.requireGlobalLogin( + this.req, + this.res, + this.next + ) + }) + + return it('should call next() directly', function() { + return this.next.called.should.equal(true) + }) + }) + + describe('with white listed url and a query string', function() { + beforeEach(function() { + this.AuthenticationController.addEndpointToLoginWhitelist('/login') + this.req._parsedUrl.pathname = '/login' + this.req.url = '/login?query=something' + return this.AuthenticationController.requireGlobalLogin( + this.req, + this.res, + this.next + ) + }) + + return it('should call next() directly', function() { + return this.next.called.should.equal(true) + }) + }) + + describe('with http auth', function() { + beforeEach(function() { + this.req.headers['authorization'] = 'Mock Basic Auth' + return this.AuthenticationController.requireGlobalLogin( + this.req, + this.res, + this.next + ) + }) + + return it('should pass the request onto httpAuth', function() { + return this.AuthenticationController.httpAuth + .calledWith(this.req, this.res, this.next) + .should.equal(true) + }) + }) + + describe('with a user session', function() { + beforeEach(function() { + this.req.session = { user: { mock: 'user', _id: 'some_id' } } + return this.AuthenticationController.requireGlobalLogin( + this.req, + this.res, + this.next + ) + }) + + return it('should call next() directly', function() { + return this.next.called.should.equal(true) + }) + }) + + return describe('with no login credentials', function() { + beforeEach(function() { + this.req.session = {} + return this.AuthenticationController.requireGlobalLogin( + this.req, + this.res, + this.next + ) + }) + + it('should have called setRedirectInSession', function() { + return this.setRedirect.callCount.should.equal(1) + }) + + return it('should redirect to the /login page', function() { + return this.res.redirectedTo.should.equal('/login') + }) + }) + }) + + describe('_redirectToLoginOrRegisterPage', function() { + beforeEach(function() { + this.middleware = this.AuthenticationController.requireLogin( + (this.options = { load_from_db: false }) + ) + this.req.session = {} + this.AuthenticationController._redirectToRegisterPage = sinon.stub() + this.AuthenticationController._redirectToLoginPage = sinon.stub() + return (this.req.query = {}) + }) + + describe('they have come directly to the url', function() { + beforeEach(function() { + this.req.query = {} + return this.middleware(this.req, this.res, this.next) + }) + + return it('should redirect to the login page', function() { + this.AuthenticationController._redirectToRegisterPage + .calledWith(this.req, this.res) + .should.equal(false) + return this.AuthenticationController._redirectToLoginPage + .calledWith(this.req, this.res) + .should.equal(true) + }) + }) + + describe('they have come via a templates link', function() { + beforeEach(function() { + this.req.query.zipUrl = 'something' + return this.middleware(this.req, this.res, this.next) + }) + + return it('should redirect to the register page', function() { + this.AuthenticationController._redirectToRegisterPage + .calledWith(this.req, this.res) + .should.equal(true) + return this.AuthenticationController._redirectToLoginPage + .calledWith(this.req, this.res) + .should.equal(false) + }) + }) + + return describe('they have been invited to a project', function() { + beforeEach(function() { + this.req.query.project_name = 'something' + return this.middleware(this.req, this.res, this.next) + }) + + return it('should redirect to the register page', function() { + this.AuthenticationController._redirectToRegisterPage + .calledWith(this.req, this.res) + .should.equal(true) + return this.AuthenticationController._redirectToLoginPage + .calledWith(this.req, this.res) + .should.equal(false) + }) + }) + }) + + describe('_redirectToRegisterPage', function() { + beforeEach(function() { + this.req.path = '/target/url' + this.req.query = { extra_query: 'foo' } + return this.AuthenticationController._redirectToRegisterPage( + this.req, + this.res + ) + }) + + it('should redirect to the register page with a query string attached', function() { + this.req.session.postLoginRedirect.should.equal( + '/target/url?extra_query=foo' + ) + return this.res.redirectedTo.should.equal('/register?extra_query=foo') + }) + + return it('should log out a message', function() { + return this.logger.log + .calledWith( + { url: this.url }, + 'user not logged in so redirecting to register page' + ) + .should.equal(true) + }) + }) + + describe('_redirectToLoginPage', function() { + beforeEach(function() { + this.req.path = '/target/url' + this.req.query = { extra_query: 'foo' } + return this.AuthenticationController._redirectToLoginPage( + this.req, + this.res + ) + }) + + return it('should redirect to the register page with a query string attached', function() { + this.req.session.postLoginRedirect.should.equal( + '/target/url?extra_query=foo' + ) + return this.res.redirectedTo.should.equal('/login?extra_query=foo') + }) + }) + + describe('_recordSuccessfulLogin', function() { + beforeEach(function() { + this.UserUpdater.updateUser = sinon.stub().callsArg(2) + return this.AuthenticationController._recordSuccessfulLogin( + this.user._id, + this.callback + ) + }) + + it('should increment the user.login.success metric', function() { + return this.Metrics.inc + .calledWith('user.login.success') + .should.equal(true) + }) + + it("should update the user's login count and last logged in date", function() { + this.UserUpdater.updateUser.args[0][1]['$set'][ + 'lastLoggedIn' + ].should.not.equal(undefined) + return this.UserUpdater.updateUser.args[0][1]['$inc'][ + 'loginCount' + ].should.equal(1) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('_recordFailedLogin', function() { + beforeEach(function() { + return this.AuthenticationController._recordFailedLogin(this.callback) + }) + + it('should increment the user.login.failed metric', function() { + return this.Metrics.inc.calledWith('user.login.failed').should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('setRedirectInSession', function() { + beforeEach(function() { + this.req = { session: {} } + this.req.path = '/somewhere' + return (this.req.query = { one: '1' }) + }) + + it('should set redirect property on session', function() { + this.AuthenticationController.setRedirectInSession(this.req) + return expect(this.req.session.postLoginRedirect).to.equal( + '/somewhere?one=1' + ) + }) + + it('should set the supplied value', function() { + this.AuthenticationController.setRedirectInSession( + this.req, + '/somewhere/specific' + ) + return expect(this.req.session.postLoginRedirect).to.equal( + '/somewhere/specific' + ) + }) + + it('should not allow open redirects', function() { + this.AuthenticationController.setRedirectInSession( + this.req, + 'https://evil.com' + ) + return expect(this.req.session.postLoginRedirect).to.be.undefined + }) + + describe('with a png', function() { + beforeEach(function() { + return (this.req = { session: {} }) + }) + + return it('should not set the redirect', function() { + this.AuthenticationController.setRedirectInSession( + this.req, + '/something.png' + ) + return expect(this.req.session.postLoginRedirect).to.equal(undefined) + }) + }) + + return describe('with a js path', function() { + beforeEach(function() { + return (this.req = { session: {} }) + }) + + return it('should not set the redirect', function() { + this.AuthenticationController.setRedirectInSession( + this.req, + '/js/something.js' + ) + return expect(this.req.session.postLoginRedirect).to.equal(undefined) + }) + }) + }) + + describe('_getRedirectFromSession', function() { + it('should get redirect property from session', function() { + this.req = { session: { postLoginRedirect: '/a?b=c' } } + return expect( + this.AuthenticationController._getRedirectFromSession(this.req) + ).to.equal('/a?b=c') + }) + + it('should not allow open redirects', function() { + this.req = { session: { postLoginRedirect: 'https://evil.com' } } + return expect( + this.AuthenticationController._getRedirectFromSession(this.req) + ).to.be.null + }) + + return it('handle null values', function() { + this.req = { session: {} } + return expect( + this.AuthenticationController._getRedirectFromSession(this.req) + ).to.be.null + }) + }) + + describe('_getSafeRedirectPath', () => + it('sanitize redirect path to prevent open redirects', function() { + expect( + this.AuthenticationController._getSafeRedirectPath('https://evil.com') + ).to.be.undefined + + expect(this.AuthenticationController._getSafeRedirectPath('//evil.com')) + .to.be.undefined + + expect( + this.AuthenticationController._getSafeRedirectPath('//ol.com/evil') + ).to.equal('/evil') + + expect(this.AuthenticationController._getSafeRedirectPath('////evil.com')) + .to.be.undefined + + expect( + this.AuthenticationController._getSafeRedirectPath('%2F%2Fevil.com') + ).to.equal('/%2F%2Fevil.com') + + return expect( + this.AuthenticationController._getSafeRedirectPath('.evil.com') + ).to.equal('/.evil.com') + })) + + describe('_clearRedirectFromSession', function() { + beforeEach(function() { + return (this.req = { session: { postLoginRedirect: '/a?b=c' } }) + }) + + return it('should remove the redirect property from session', function() { + this.AuthenticationController._clearRedirectFromSession(this.req) + return expect(this.req.session.postLoginRedirect).to.equal(undefined) + }) + }) + + return describe('finishLogin', function() { + // - get redirect + // - async handlers + // - afterLoginSessionSetup + // - clear redirect + // - issue redir, two ways + beforeEach(function() { + this.AuthenticationController._getRedirectFromSession = sinon + .stub() + .returns('/some/page') + this.AuthenticationController._loginAsyncHandlers = sinon.stub() + this.AuthenticationController.afterLoginSessionSetup = sinon + .stub() + .callsArgWith(2, null) + this.AuthenticationController._clearRedirectFromSession = sinon.stub() + this.AuthenticationController._redirectToReconfirmPage = sinon.stub() + this.req.headers = { accept: 'application/json, whatever' } + this.res.json = sinon.stub() + return (this.res.redirect = sinon.stub()) + }) + + it('should extract the redirect from the session', function() { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + expect( + this.AuthenticationController._getRedirectFromSession.callCount + ).to.equal(1) + return expect( + this.AuthenticationController._getRedirectFromSession.calledWith( + this.req + ) + ).to.equal(true) + }) + + it('should call the async handlers', function() { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + expect( + this.AuthenticationController._loginAsyncHandlers.callCount + ).to.equal(1) + return expect( + this.AuthenticationController._loginAsyncHandlers.calledWith( + this.req, + this.user + ) + ).to.equal(true) + }) + + it('should call afterLoginSessionSetup', function() { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + expect( + this.AuthenticationController.afterLoginSessionSetup.callCount + ).to.equal(1) + return expect( + this.AuthenticationController.afterLoginSessionSetup.calledWith( + this.req, + this.user + ) + ).to.equal(true) + }) + + it('should clear redirect from session', function() { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + expect( + this.AuthenticationController._clearRedirectFromSession.callCount + ).to.equal(1) + return expect( + this.AuthenticationController._clearRedirectFromSession.calledWith( + this.req + ) + ).to.equal(true) + }) + + it('should issue a json response with a redirect', function() { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + expect(this.res.json.callCount).to.equal(1) + expect(this.res.redirect.callCount).to.equal(0) + return expect(this.res.json.calledWith({ redir: '/some/page' })).to.equal( + true + ) + }) + + describe('with a non-json request', function() { + beforeEach(function() { + this.req.headers = {} + this.res.json = sinon.stub() + return (this.res.redirect = sinon.stub()) + }) + + return it('should issue a plain redirect', function() { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + expect(this.res.json.callCount).to.equal(0) + expect(this.res.redirect.callCount).to.equal(1) + return expect(this.res.redirect.calledWith('/some/page')).to.equal(true) + }) + }) + + return describe('when user is flagged to reconfirm', function() { + beforeEach(function() { + this.req.session = {} + return (this.user.must_reconfirm = true) + }) + return it('should redirect to reconfirm page', function() { + this.AuthenticationController.finishLogin( + this.user, + this.req, + this.res, + this.next + ) + return expect( + this.AuthenticationController._redirectToReconfirmPage.calledWith( + this.req + ) + ).to.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Authentication/AuthenticationManagerTests.js b/services/web/test/unit/src/Authentication/AuthenticationManagerTests.js new file mode 100644 index 0000000000..32aae33452 --- /dev/null +++ b/services/web/test/unit/src/Authentication/AuthenticationManagerTests.js @@ -0,0 +1,685 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Authentication/AuthenticationManager.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const { ObjectId } = require('mongojs') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('AuthenticationManager', function() { + beforeEach(function() { + this.settings = { security: { bcryptRounds: 12 } } + this.AuthenticationManager = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: (this.User = {}) + }, + '../../infrastructure/mongojs': { + db: (this.db = { users: {} }), + ObjectId + }, + bcrypt: (this.bcrypt = {}), + 'settings-sharelatex': this.settings, + '../V1/V1Handler': (this.V1Handler = {}), + '../User/UserGetter': (this.UserGetter = {}) + } + }) + return (this.callback = sinon.stub()) + }) + + describe('with real bcrypt', function() { + beforeEach(function() { + const bcrypt = require('bcrypt') + this.bcrypt.compare = bcrypt.compare + this.bcrypt.getRounds = bcrypt.getRounds + this.bcrypt.genSalt = bcrypt.genSalt + this.bcrypt.hash = bcrypt.hash + // Hash of 'testpassword' + return (this.testPassword = + '$2a$12$zhtThy3R5tLtw5sCwr5XD.zhPENGn4ecjeMcP87oYSYrIICFqBpei') + }) + + describe('authenticate', function() { + beforeEach(function() { + this.user = { + _id: 'user-id', + email: (this.email = 'USER@sharelatex.com') + } + return (this.User.findOne = sinon + .stub() + .callsArgWith(1, null, this.user)) + }) + + describe('when the hashed password matches', function() { + beforeEach(function(done) { + this.unencryptedPassword = 'testpassword' + this.user.hashedPassword = this.testPassword + return this.AuthenticationManager.authenticate( + { email: this.email }, + this.unencryptedPassword, + (error, user) => { + this.callback(error, user) + return done() + } + ) + }) + + it('should look up the correct user in the database', function() { + return this.User.findOne + .calledWith({ email: this.email }) + .should.equal(true) + }) + + return it('should return the user', function() { + return this.callback.calledWith(null, this.user).should.equal(true) + }) + }) + + return describe('when the encrypted passwords do not match', function() { + beforeEach(function() { + return this.AuthenticationManager.authenticate( + { email: this.email }, + 'notthecorrectpassword', + this.callback + ) + }) + + return it('should not return the user', function() { + return this.callback.calledWith(null, null).should.equal(true) + }) + }) + }) + + return describe('setUserPasswordInV2', function() { + beforeEach(function() { + this.user = { + _id: '5c8791477192a80b5e76ca7e', + email: (this.email = 'USER@sharelatex.com') + } + return (this.db.users.update = sinon + .stub() + .callsArgWith(2, null, { nModified: 1 })) + }) + + it('should not produce an error', function(done) { + return this.AuthenticationManager.setUserPasswordInV2( + this.user._id, + 'testpassword', + (err, updated) => { + expect(err).to.not.exist + expect(updated).to.equal(true) + return done() + } + ) + }) + + return it('should set the hashed password', function(done) { + return this.AuthenticationManager.setUserPasswordInV2( + this.user._id, + 'testpassword', + (err, updated) => { + expect(err).to.not.exist + const { + hashedPassword + } = this.db.users.update.lastCall.args[1].$set + expect(hashedPassword).to.exist + expect(hashedPassword.length).to.equal(60) + expect(hashedPassword).to.match(/^\$2a\$12\$[a-zA-Z0-9\/.]{53}$/) + return done() + } + ) + }) + }) + }) + + describe('authenticate', function() { + describe('when the user exists in the database', function() { + beforeEach(function() { + this.user = { + _id: 'user-id', + email: (this.email = 'USER@sharelatex.com') + } + this.unencryptedPassword = 'banana' + return (this.User.findOne = sinon + .stub() + .callsArgWith(1, null, this.user)) + }) + + describe('when the hashed password matches', function() { + beforeEach(function(done) { + this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf' + this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true) + this.bcrypt.getRounds = sinon.stub().returns(12) + return this.AuthenticationManager.authenticate( + { email: this.email }, + this.unencryptedPassword, + (error, user) => { + this.callback(error, user) + return done() + } + ) + }) + + it('should look up the correct user in the database', function() { + return this.User.findOne + .calledWith({ email: this.email }) + .should.equal(true) + }) + + it('should check that the passwords match', function() { + return this.bcrypt.compare + .calledWith(this.unencryptedPassword, this.hashedPassword) + .should.equal(true) + }) + + return it('should return the user', function() { + return this.callback.calledWith(null, this.user).should.equal(true) + }) + }) + + describe('when the encrypted passwords do not match', function() { + beforeEach(function() { + return this.AuthenticationManager.authenticate( + { email: this.email }, + this.unencryptedPassword, + this.callback + ) + }) + + return it('should not return the user', function() { + return this.callback.calledWith(null, null).should.equal(true) + }) + }) + + describe('when the hashed password matches but the number of rounds is too low', function() { + beforeEach(function(done) { + this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf' + this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true) + this.bcrypt.getRounds = sinon.stub().returns(7) + this.AuthenticationManager.setUserPassword = sinon + .stub() + .callsArgWith(2, null) + return this.AuthenticationManager.authenticate( + { email: this.email }, + this.unencryptedPassword, + (error, user) => { + this.callback(error, user) + return done() + } + ) + }) + + it('should look up the correct user in the database', function() { + return this.User.findOne + .calledWith({ email: this.email }) + .should.equal(true) + }) + + it('should check that the passwords match', function() { + return this.bcrypt.compare + .calledWith(this.unencryptedPassword, this.hashedPassword) + .should.equal(true) + }) + + it('should check the number of rounds', function() { + return this.bcrypt.getRounds.called.should.equal(true) + }) + + it('should set the users password (with a higher number of rounds)', function() { + return this.AuthenticationManager.setUserPassword + .calledWith('user-id', this.unencryptedPassword) + .should.equal(true) + }) + + return it('should return the user', function() { + return this.callback.calledWith(null, this.user).should.equal(true) + }) + }) + + return describe('when the hashed password matches but the number of rounds is too low, but upgrades disabled', function() { + beforeEach(function(done) { + this.settings.security.disableBcryptRoundsUpgrades = true + this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf' + this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true) + this.bcrypt.getRounds = sinon.stub().returns(7) + this.AuthenticationManager.setUserPassword = sinon + .stub() + .callsArgWith(2, null) + return this.AuthenticationManager.authenticate( + { email: this.email }, + this.unencryptedPassword, + (error, user) => { + this.callback(error, user) + return done() + } + ) + }) + + it('should not check the number of rounds', function() { + return this.bcrypt.getRounds.called.should.equal(false) + }) + + it('should not set the users password (with a higher number of rounds)', function() { + return this.AuthenticationManager.setUserPassword + .calledWith('user-id', this.unencryptedPassword) + .should.equal(false) + }) + + return it('should return the user', function() { + return this.callback.calledWith(null, this.user).should.equal(true) + }) + }) + }) + + return describe('when the user does not exist in the database', function() { + beforeEach(function() { + this.User.findOne = sinon.stub().callsArgWith(1, null, null) + return this.AuthenticationManager.authenticate( + { email: this.email }, + this.unencrpytedPassword, + this.callback + ) + }) + + return it('should not return a user', function() { + return this.callback.calledWith(null, null).should.equal(true) + }) + }) + }) + + describe('validateEmail', function() { + describe('valid', () => + it('should return null', function() { + const result = this.AuthenticationManager.validateEmail( + 'foo@example.com' + ) + return expect(result).to.equal(null) + })) + + return describe('invalid', function() { + it('should return validation error object for no email', function() { + const result = this.AuthenticationManager.validateEmail('') + expect(result).to.not.equal(null) + return expect(result.message).to.equal('email not valid') + }) + + return it('should return validation error object for invalid', function() { + const result = this.AuthenticationManager.validateEmail('notanemail') + expect(result).to.not.equal(null) + return expect(result.message).to.equal('email not valid') + }) + }) + }) + + describe('validatePassword', function() { + beforeEach(function() { + // 73 characters: + return (this.longPassword = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345678') + }) + + describe('with a null password', () => + it('should return an error', function() { + return expect(this.AuthenticationManager.validatePassword()).to.eql({ + message: 'password not set' + }) + })) + + describe('password length', function() { + describe('with the default password length options', function() { + it('should reject passwords that are too short', function() { + expect(this.AuthenticationManager.validatePassword('')).to.eql({ + message: 'password is too short' + }) + return expect( + this.AuthenticationManager.validatePassword('foo') + ).to.eql({ message: 'password is too short' }) + }) + + it('should reject passwords that are too long', function() { + return expect( + this.AuthenticationManager.validatePassword(this.longPassword) + ).to.eql({ message: 'password is too long' }) + }) + + return it('should accept passwords that are a good length', function() { + return expect( + this.AuthenticationManager.validatePassword('l337h4x0r') + ).to.equal(null) + }) + }) + + describe('when the password length is specified in settings', function() { + beforeEach(function() { + return (this.settings.passwordStrengthOptions = { + length: { + min: 10, + max: 12 + } + }) + }) + + it('should reject passwords that are too short', function() { + return expect( + this.AuthenticationManager.validatePassword('012345678') + ).to.eql({ message: 'password is too short' }) + }) + + it('should accept passwords of exactly minimum length', function() { + return expect( + this.AuthenticationManager.validatePassword('0123456789') + ).to.equal(null) + }) + + it('should reject passwords that are too long', function() { + return expect( + this.AuthenticationManager.validatePassword('0123456789abc') + ).to.eql({ message: 'password is too long' }) + }) + + return it('should accept passwords of exactly maximum length', function() { + return expect( + this.AuthenticationManager.validatePassword('0123456789ab') + ).to.equal(null) + }) + }) + + return describe('when the maximum password length is set to >72 characters in settings', function() { + beforeEach(function() { + return (this.settings.passwordStrengthOptions = { + length: { + max: 128 + } + }) + }) + + return it('should still reject passwords > 72 characters in length', function() { + return expect( + this.AuthenticationManager.validatePassword(this.longPassword) + ).to.eql({ message: 'password is too long' }) + }) + }) + }) + + return describe('allowed characters', function() { + describe('with the default settings for allowed characters', function() { + it('should allow passwords with valid characters', function() { + expect( + this.AuthenticationManager.validatePassword( + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + ) + ).to.equal(null) + return expect( + this.AuthenticationManager.validatePassword( + '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,' + ) + ).to.equal(null) + }) + + return it('should not allow passwords with invalid characters', function() { + return expect( + this.AuthenticationManager.validatePassword( + 'correct horse battery staple' + ) + ).to.eql({ message: 'password contains an invalid character' }) + }) + }) + + describe('when valid characters are overridden in settings', function() { + beforeEach(function() { + return (this.settings.passwordStrengthOptions = { + chars: { + symbols: ' ' + } + }) + }) + + it('should allow passwords with valid characters', function() { + return expect( + this.AuthenticationManager.validatePassword( + 'correct horse battery staple' + ) + ).to.equal(null) + }) + + return it('should disallow passwords with invalid characters', function() { + return expect( + this.AuthenticationManager.validatePassword( + '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,' + ) + ).to.eql({ message: 'password contains an invalid character' }) + }) + }) + + return describe('when allowAnyChars is set', function() { + beforeEach(function() { + return (this.settings.passwordStrengthOptions = { + allowAnyChars: true + }) + }) + + return it('should allow any characters', function() { + expect( + this.AuthenticationManager.validatePassword( + 'correct horse battery staple' + ) + ).to.equal(null) + return expect( + this.AuthenticationManager.validatePassword( + '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,' + ) + ).to.equal(null) + }) + }) + }) + }) + + return describe('setUserPassword', function() { + beforeEach(function() { + this.user_id = ObjectId() + this.password = 'banana' + this.hashedPassword = 'asdkjfa;osiuvandf' + this.salt = 'saltaasdfasdfasdf' + this.bcrypt.genSalt = sinon.stub().callsArgWith(2, null, this.salt) + this.bcrypt.hash = sinon.stub().callsArgWith(2, null, this.hashedPassword) + return (this.db.users.update = sinon.stub().callsArg(2)) + }) + + describe('too long', function() { + beforeEach(function() { + this.settings.passwordStrengthOptions = { + length: { + max: 10 + } + } + return (this.password = 'dsdsadsadsadsadsadkjsadjsadjsadljs') + }) + + it('should return and error', function(done) { + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + function(err) { + expect(err).to.exist + return done() + } + ) + }) + + return it('should not start the bcrypt process', function(done) { + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + err => { + this.bcrypt.genSalt.called.should.equal(false) + this.bcrypt.hash.called.should.equal(false) + return done() + } + ) + }) + }) + + describe('too short', function() { + beforeEach(function() { + this.settings.passwordStrengthOptions = { + length: { + max: 10, + min: 6 + } + } + return (this.password = 'dsd') + }) + + it('should return and error', function(done) { + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + function(err) { + expect(err).to.exist + return done() + } + ) + }) + + return it('should not start the bcrypt process', function(done) { + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + err => { + this.bcrypt.genSalt.called.should.equal(false) + this.bcrypt.hash.called.should.equal(false) + return done() + } + ) + }) + }) + + return describe('password set attempt', function() { + describe('with SL user in SL', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon + .stub() + .yields(null, { overleaf: null }) + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + this.callback + ) + }) + + it('should look up the user', function() { + return this.UserGetter.getUser + .calledWith(this.user_id) + .should.equal(true) + }) + + it("should update the user's password in the database", function() { + const { args } = this.db.users.update.lastCall + expect(args[0]).to.deep.equal({ + _id: ObjectId(this.user_id.toString()) + }) + return expect(args[1]).to.deep.equal({ + $set: { + hashedPassword: this.hashedPassword + }, + $unset: { + password: true + } + }) + }) + + it('should hash the password', function() { + this.bcrypt.genSalt.calledWith(12).should.equal(true) + return this.bcrypt.hash + .calledWith(this.password, this.salt) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('with SL user in v2', function() { + beforeEach(function(done) { + this.settings.overleaf = true + this.UserGetter.getUser = sinon + .stub() + .yields(null, { overleaf: null }) + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + (err, changed) => { + this.callback(err, changed) + return done() + } + ) + }) + return it('should error', function() { + return this.callback + .calledWith(new Errors.SLInV2Error('Password Reset Attempt')) + .should.equal(true) + }) + }) + + describe('with v2 user in SL', function() { + beforeEach(function(done) { + this.UserGetter.getUser = sinon + .stub() + .yields(null, { overleaf: { id: 1 } }) + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + (err, changed) => { + this.callback(err, changed) + return done() + } + ) + }) + return it('should error', function() { + return this.callback + .calledWith(new Errors.NotInV2Error('Password Reset Attempt')) + .should.equal(true) + }) + }) + + return describe('with v2 user in v2', function() { + beforeEach(function(done) { + this.settings.overleaf = true + this.UserGetter.getUser = sinon + .stub() + .yields(null, { overleaf: { id: 1 } }) + this.V1Handler.doPasswordReset = sinon.stub().yields(null, true) + return this.AuthenticationManager.setUserPassword( + this.user_id, + this.password, + (err, changed) => { + this.callback(err, changed) + return done() + } + ) + }) + return it('should set the password in v2', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js b/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js new file mode 100644 index 0000000000..77d94d9d4c --- /dev/null +++ b/services/web/test/unit/src/Authorization/AuthorizationManagerTests.js @@ -0,0 +1,979 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Authorization/AuthorizationManager.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/src/Features/Errors/Errors.js') + +describe('AuthorizationManager', function() { + beforeEach(function() { + this.AuthorizationManager = SandboxedModule.require(modulePath, { + requires: { + '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../../models/User': { + User: (this.User = {}) + }, + '../Errors/Errors': Errors, + '../TokenAccess/TokenAccessHandler': (this.TokenAccessHandler = { + isValidToken: sinon.stub().callsArgWith(2, null, false, false) + }) + } + }) + this.user_id = 'user-id-1' + this.project_id = 'project-id-1' + this.token = 'some-token' + return (this.callback = sinon.stub()) + }) + + describe('getPrivilegeLevelForProject', function() { + beforeEach(function() { + this.ProjectGetter.getProject = sinon.stub() + this.AuthorizationManager.isUserSiteAdmin = sinon.stub() + return (this.CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub()) + }) + + describe('with a token-based project', function() { + beforeEach(function() { + return this.ProjectGetter.getProject + .withArgs(this.project_id, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: 'tokenBased' }) + }) + + describe('with a user_id with a privilege level', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, 'readOnly') + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it("should return the user's privilege level", function() { + return this.callback + .calledWith(null, 'readOnly', false, false) + .should.equal(true) + }) + }) + + describe('with a user_id with no privilege level', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it('should return false', function() { + return this.callback + .calledWith(null, false, false, false) + .should.equal(true) + }) + }) + + describe('with a user_id who is an admin', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, true) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it('should return the user as an owner', function() { + return this.callback + .calledWith(null, 'owner', false, true) + .should.equal(true) + }) + }) + + return describe('with no user (anonymous)', function() { + describe('when the token is not valid', function() { + beforeEach(function() { + this.TokenAccessHandler.isValidToken = sinon + .stub() + .withArgs(this.project_id, this.token) + .yields(null, false, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + null, + this.project_id, + this.token, + this.callback + ) + }) + + it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + false + ) + }) + + it('should not call AuthorizationManager.isUserSiteAdmin', function() { + return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + false + ) + }) + + it('should check if the token is valid', function() { + return this.TokenAccessHandler.isValidToken + .calledWith(this.project_id, this.token) + .should.equal(true) + }) + + return it('should return false', function() { + return this.callback + .calledWith(null, false, false, false) + .should.equal(true) + }) + }) + + describe('when the token is valid for read-and-write', function() { + describe('when read-write-sharing is not enabled', function() { + beforeEach(function() { + this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false + this.TokenAccessHandler.isValidToken = sinon + .stub() + .withArgs(this.project_id, this.token) + .yields(null, true, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + null, + this.project_id, + this.token, + this.callback + ) + }) + + it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + false + ) + }) + + it('should not call AuthorizationManager.isUserSiteAdmin', function() { + return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + false + ) + }) + + it('should check if the token is valid', function() { + return this.TokenAccessHandler.isValidToken + .calledWith(this.project_id, this.token) + .should.equal(true) + }) + + return it('should deny access', function() { + return this.callback + .calledWith(null, false, false, false) + .should.equal(true) + }) + }) + + return describe('when read-write-sharing is enabled', function() { + beforeEach(function() { + this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true + this.TokenAccessHandler.isValidToken = sinon + .stub() + .withArgs(this.project_id, this.token) + .yields(null, true, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + null, + this.project_id, + this.token, + this.callback + ) + }) + + it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + false + ) + }) + + it('should not call AuthorizationManager.isUserSiteAdmin', function() { + return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + false + ) + }) + + it('should check if the token is valid', function() { + return this.TokenAccessHandler.isValidToken + .calledWith(this.project_id, this.token) + .should.equal(true) + }) + + return it('should give read-write access', function() { + return this.callback + .calledWith(null, 'readAndWrite', false) + .should.equal(true) + }) + }) + }) + + return describe('when the token is valid for read-only', function() { + beforeEach(function() { + this.TokenAccessHandler.isValidToken = sinon + .stub() + .withArgs(this.project_id, this.token) + .yields(null, false, true) + return this.AuthorizationManager.getPrivilegeLevelForProject( + null, + this.project_id, + this.token, + this.callback + ) + }) + + it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + false + ) + }) + + it('should not call AuthorizationManager.isUserSiteAdmin', function() { + return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + false + ) + }) + + it('should check if the token is valid', function() { + return this.TokenAccessHandler.isValidToken + .calledWith(this.project_id, this.token) + .should.equal(true) + }) + + return it('should give read-only access', function() { + return this.callback + .calledWith(null, 'readOnly', false) + .should.equal(true) + }) + }) + }) + }) + + describe('with a private project', function() { + beforeEach(function() { + return this.ProjectGetter.getProject + .withArgs(this.project_id, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: 'private' }) + }) + + describe('with a user_id with a privilege level', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, 'readOnly') + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it("should return the user's privilege level", function() { + return this.callback + .calledWith(null, 'readOnly', false, false) + .should.equal(true) + }) + }) + + describe('with a user_id with no privilege level', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it('should return false', function() { + return this.callback + .calledWith(null, false, false, false) + .should.equal(true) + }) + }) + + describe('with a user_id who is an admin', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, true) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it('should return the user as an owner', function() { + return this.callback + .calledWith(null, 'owner', false, true) + .should.equal(true) + }) + }) + + return describe('with no user (anonymous)', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject( + null, + this.project_id, + this.token, + this.callback + ) + }) + + it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + false + ) + }) + + it('should not call AuthorizationManager.isUserSiteAdmin', function() { + return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + false + ) + }) + + return it('should return false', function() { + return this.callback + .calledWith(null, false, false, false) + .should.equal(true) + }) + }) + }) + + describe('with a public project', function() { + beforeEach(function() { + return this.ProjectGetter.getProject + .withArgs(this.project_id, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: 'readAndWrite' }) + }) + + describe('with a user_id with a privilege level', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, 'readOnly') + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it("should return the user's privilege level", function() { + return this.callback + .calledWith(null, 'readOnly', false) + .should.equal(true) + }) + }) + + describe('with a user_id with no privilege level', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it('should return the public privilege level', function() { + return this.callback + .calledWith(null, 'readAndWrite', true) + .should.equal(true) + }) + }) + + describe('with a user_id who is an admin', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, true) + this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, false) + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + this.callback + ) + }) + + return it('should return the user as an owner', function() { + return this.callback + .calledWith(null, 'owner', false) + .should.equal(true) + }) + }) + + return describe('with no user (anonymous)', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject( + null, + this.project_id, + this.token, + this.callback + ) + }) + + it('should not call CollaboratorsHandler.getMemberIdPrivilegeLevel', function() { + return this.CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal( + false + ) + }) + + it('should not call AuthorizationManager.isUserSiteAdmin', function() { + return this.AuthorizationManager.isUserSiteAdmin.called.should.equal( + false + ) + }) + + return it('should return the public privilege level', function() { + return this.callback + .calledWith(null, 'readAndWrite', true) + .should.equal(true) + }) + }) + }) + + describe("when the project doesn't exist", function() { + beforeEach(function() { + return this.ProjectGetter.getProject + .withArgs(this.project_id, { publicAccesLevel: 1 }) + .yields(null, null) + }) + + return it('should return a NotFoundError', function() { + return this.AuthorizationManager.getPrivilegeLevelForProject( + this.user_id, + this.project_id, + this.token, + error => error.should.be.instanceof(Errors.NotFoundError) + ) + }) + }) + + return describe('when the project id is not valid', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + return this.CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(this.user_id, this.project_id) + .yields(null, 'readOnly') + }) + + return it('should return a error', function(done) { + return this.AuthorizationManager.getPrivilegeLevelForProject( + undefined, + 'not project id', + this.token, + err => { + this.ProjectGetter.getProject.called.should.equal(false) + expect(err).to.exist + return done() + } + ) + }) + }) + }) + + describe('canUserReadProject', function() { + beforeEach(function() { + return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) + }) + + describe('when user is owner', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'owner', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserReadProject( + this.user_id, + this.project_id, + this.token, + function(error, canRead) { + expect(canRead).to.equal(true) + return done() + } + ) + }) + }) + + describe('when user has read-write access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readAndWrite', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserReadProject( + this.user_id, + this.project_id, + this.token, + function(error, canRead) { + expect(canRead).to.equal(true) + return done() + } + ) + }) + }) + + describe('when user has read-only access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readOnly', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserReadProject( + this.user_id, + this.project_id, + this.token, + function(error, canRead) { + expect(canRead).to.equal(true) + return done() + } + ) + }) + }) + + return describe('when user has no access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, false, false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserReadProject( + this.user_id, + this.project_id, + this.token, + function(error, canRead) { + expect(canRead).to.equal(false) + return done() + } + ) + }) + }) + }) + + describe('canUserWriteProjectContent', function() { + beforeEach(function() { + return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) + }) + + describe('when user is owner', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'owner', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserWriteProjectContent( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(true) + return done() + } + ) + }) + }) + + describe('when user has read-write access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readAndWrite', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserWriteProjectContent( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(true) + return done() + } + ) + }) + }) + + describe('when user has read-only access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readOnly', false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserWriteProjectContent( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(false) + return done() + } + ) + }) + }) + + return describe('when user has no access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, false, false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserWriteProjectContent( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(false) + return done() + } + ) + }) + }) + }) + + describe('canUserWriteProjectSettings', function() { + beforeEach(function() { + return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) + }) + + describe('when user is owner', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'owner', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserWriteProjectSettings( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(true) + return done() + } + ) + }) + }) + + describe('when user has read-write access as a collaborator', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readAndWrite', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserWriteProjectSettings( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(true) + return done() + } + ) + }) + }) + + describe('when user has read-write access as the public', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readAndWrite', true) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserWriteProjectSettings( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(false) + return done() + } + ) + }) + }) + + describe('when user has read-only access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readOnly', false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserWriteProjectSettings( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(false) + return done() + } + ) + }) + }) + + return describe('when user has no access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, false, false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserWriteProjectSettings( + this.user_id, + this.project_id, + this.token, + function(error, canWrite) { + expect(canWrite).to.equal(false) + return done() + } + ) + }) + }) + }) + + describe('canUserAdminProject', function() { + beforeEach(function() { + return (this.AuthorizationManager.getPrivilegeLevelForProject = sinon.stub()) + }) + + describe('when user is owner', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'owner', false) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.canUserAdminProject( + this.user_id, + this.project_id, + this.token, + function(error, canAdmin) { + expect(canAdmin).to.equal(true) + return done() + } + ) + }) + }) + + describe('when user has read-write access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readAndWrite', false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserAdminProject( + this.user_id, + this.project_id, + this.token, + function(error, canAdmin) { + expect(canAdmin).to.equal(false) + return done() + } + ) + }) + }) + + describe('when user has read-only access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, 'readOnly', false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserAdminProject( + this.user_id, + this.project_id, + this.token, + function(error, canAdmin) { + expect(canAdmin).to.equal(false) + return done() + } + ) + }) + }) + + return describe('when user has no access', function() { + beforeEach(function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, false, false) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.canUserAdminProject( + this.user_id, + this.project_id, + this.token, + function(error, canAdmin) { + expect(canAdmin).to.equal(false) + return done() + } + ) + }) + }) + }) + + return describe('isUserSiteAdmin', function() { + beforeEach(function() { + return (this.User.findOne = sinon.stub()) + }) + + describe('when user is admin', function() { + beforeEach(function() { + return this.User.findOne + .withArgs({ _id: this.user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: true }) + }) + + return it('should return true', function(done) { + return this.AuthorizationManager.isUserSiteAdmin(this.user_id, function( + error, + isAdmin + ) { + expect(isAdmin).to.equal(true) + return done() + }) + }) + }) + + describe('when user is not admin', function() { + beforeEach(function() { + return this.User.findOne + .withArgs({ _id: this.user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: false }) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.isUserSiteAdmin(this.user_id, function( + error, + isAdmin + ) { + expect(isAdmin).to.equal(false) + return done() + }) + }) + }) + + describe('when user is not found', function() { + beforeEach(function() { + return this.User.findOne + .withArgs({ _id: this.user_id }, { isAdmin: 1 }) + .yields(null, null) + }) + + return it('should return false', function(done) { + return this.AuthorizationManager.isUserSiteAdmin(this.user_id, function( + error, + isAdmin + ) { + expect(isAdmin).to.equal(false) + return done() + }) + }) + }) + + return describe('when no user is passed', () => + it('should return false', function(done) { + return this.AuthorizationManager.isUserSiteAdmin( + null, + (error, isAdmin) => { + this.User.findOne.called.should.equal(false) + expect(isAdmin).to.equal(false) + return done() + } + ) + })) + }) +}) diff --git a/services/web/test/unit/src/Authorization/AuthorizationMiddlewareTests.js b/services/web/test/unit/src/Authorization/AuthorizationMiddlewareTests.js new file mode 100644 index 0000000000..4e4997eb78 --- /dev/null +++ b/services/web/test/unit/src/Authorization/AuthorizationMiddlewareTests.js @@ -0,0 +1,440 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Authorization/AuthorizationMiddleware.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/src/Features/Errors/Errors.js') + +describe('AuthorizationMiddleware', function() { + beforeEach(function() { + this.user_id = 'user-id-123' + this.project_id = 'project-id-123' + this.token = 'some-token' + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user_id), + isUserLoggedIn: sinon.stub().returns(true) + } + this.AuthorizationMiddleware = SandboxedModule.require(modulePath, { + requires: { + './AuthorizationManager': (this.AuthorizationManager = {}), + 'logger-sharelatex': { log() {} }, + mongojs: { + ObjectId: (this.ObjectId = {}) + }, + '../Errors/Errors': Errors, + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../TokenAccess/TokenAccessHandler': (this.TokenAccessHandler = { + getRequestToken: sinon.stub().returns(this.token) + }) + } + }) + this.req = {} + this.res = {} + this.ObjectId.isValid = sinon.stub() + this.ObjectId.isValid.withArgs(this.project_id).returns(true) + return (this.next = sinon.stub()) + }) + + describe('_getUserId', function() { + beforeEach(function() { + return (this.req = {}) + }) + + it('should get the user from session', function(done) { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns('1234') + return this.AuthorizationMiddleware._getUserId( + this.req, + (err, user_id) => { + expect(err).to.not.exist + expect(user_id).to.equal('1234') + return done() + } + ) + }) + + it('should get oauth_user from request', function(done) { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(null) + this.req.oauth_user = { _id: '5678' } + return this.AuthorizationMiddleware._getUserId( + this.req, + (err, user_id) => { + expect(err).to.not.exist + expect(user_id).to.equal('5678') + return done() + } + ) + }) + + return it('should fall back to null', function(done) { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(null) + this.req.oauth_user = undefined + return this.AuthorizationMiddleware._getUserId( + this.req, + (err, user_id) => { + expect(err).to.not.exist + expect(user_id).to.equal(null) + return done() + } + ) + }) + }) + + const METHODS_TO_TEST = { + ensureUserCanReadProject: 'canUserReadProject', + ensureUserCanWriteProjectSettings: 'canUserWriteProjectSettings', + ensureUserCanWriteProjectContent: 'canUserWriteProjectContent', + ensureUserCanAdminProject: 'canUserAdminProject' + } + for (let middlewareMethod in METHODS_TO_TEST) { + const managerMethod = METHODS_TO_TEST[middlewareMethod] + ;((middlewareMethod, managerMethod) => + describe(middlewareMethod, function() { + beforeEach(function() { + this.req.params = { project_id: this.project_id } + this.AuthorizationManager[managerMethod] = sinon.stub() + return (this.AuthorizationMiddleware.redirectToRestricted = sinon.stub()) + }) + + describe('with missing project_id', function() { + beforeEach(function() { + return (this.req.params = {}) + }) + + return it('should return an error to next', function() { + this.AuthorizationMiddleware[middlewareMethod]( + this.req, + this.res, + this.next + ) + return this.next.calledWith(new Error()).should.equal(true) + }) + }) + + describe('with logged in user', function() { + beforeEach(function() { + return this.AuthenticationController.getLoggedInUserId.returns( + this.user_id + ) + }) + + describe('when user has permission', function() { + beforeEach(function() { + return this.AuthorizationManager[managerMethod] + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, true) + }) + + return it('should return next', function() { + this.AuthorizationMiddleware[middlewareMethod]( + this.req, + this.res, + this.next + ) + return this.next.called.should.equal(true) + }) + }) + + return describe("when user doesn't have permission", function() { + beforeEach(function() { + return this.AuthorizationManager[managerMethod] + .withArgs(this.user_id, this.project_id, this.token) + .yields(null, false) + }) + + return it('should redirect to redirectToRestricted', function() { + this.AuthorizationMiddleware[middlewareMethod]( + this.req, + this.res, + this.next + ) + this.next.called.should.equal(false) + return this.AuthorizationMiddleware.redirectToRestricted + .calledWith(this.req, this.res, this.next) + .should.equal(true) + }) + }) + }) + + describe('with anonymous user', function() { + describe('when user has permission', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId.returns(null) + return this.AuthorizationManager[managerMethod] + .withArgs(null, this.project_id, this.token) + .yields(null, true) + }) + + return it('should return next', function() { + this.AuthorizationMiddleware[middlewareMethod]( + this.req, + this.res, + this.next + ) + return this.next.called.should.equal(true) + }) + }) + + return describe("when user doesn't have permission", function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId.returns(null) + return this.AuthorizationManager[managerMethod] + .withArgs(null, this.project_id, this.token) + .yields(null, false) + }) + + return it('should redirect to redirectToRestricted', function() { + this.AuthorizationMiddleware[middlewareMethod]( + this.req, + this.res, + this.next + ) + this.next.called.should.equal(false) + return this.AuthorizationMiddleware.redirectToRestricted + .calledWith(this.req, this.res, this.next) + .should.equal(true) + }) + }) + }) + + return describe('with malformed project id', function() { + beforeEach(function() { + this.req.params = { project_id: 'blah' } + return (this.ObjectId.isValid = sinon.stub().returns(false)) + }) + + return it('should return a not found error', function(done) { + return this.AuthorizationMiddleware[middlewareMethod]( + this.req, + this.res, + function(error) { + error.should.be.instanceof(Errors.NotFoundError) + return done() + } + ) + }) + }) + }))(middlewareMethod, managerMethod) + } + + describe('ensureUserIsSiteAdmin', function() { + beforeEach(function() { + this.AuthorizationManager.isUserSiteAdmin = sinon.stub() + return (this.AuthorizationMiddleware.redirectToRestricted = sinon.stub()) + }) + + describe('with logged in user', function() { + beforeEach(function() { + return this.AuthenticationController.getLoggedInUserId.returns( + this.user_id + ) + }) + + describe('when user has permission', function() { + beforeEach(function() { + return this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, true) + }) + + return it('should return next', function() { + this.AuthorizationMiddleware.ensureUserIsSiteAdmin( + this.req, + this.res, + this.next + ) + return this.next.called.should.equal(true) + }) + }) + + return describe("when user doesn't have permission", function() { + beforeEach(function() { + return this.AuthorizationManager.isUserSiteAdmin + .withArgs(this.user_id) + .yields(null, false) + }) + + return it('should redirect to redirectToRestricted', function() { + this.AuthorizationMiddleware.ensureUserIsSiteAdmin( + this.req, + this.res, + this.next + ) + this.next.called.should.equal(false) + return this.AuthorizationMiddleware.redirectToRestricted + .calledWith(this.req, this.res, this.next) + .should.equal(true) + }) + }) + }) + + return describe('with anonymous user', function() { + describe('when user has permission', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId.returns(null) + return this.AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, true) + }) + + return it('should return next', function() { + this.AuthorizationMiddleware.ensureUserIsSiteAdmin( + this.req, + this.res, + this.next + ) + return this.next.called.should.equal(true) + }) + }) + + return describe("when user doesn't have permission", function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId.returns(null) + return this.AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, false) + }) + + return it('should redirect to redirectToRestricted', function() { + this.AuthorizationMiddleware.ensureUserIsSiteAdmin( + this.req, + this.res, + this.next + ) + this.next.called.should.equal(false) + return this.AuthorizationMiddleware.redirectToRestricted + .calledWith(this.req, this.res, this.next) + .should.equal(true) + }) + }) + }) + }) + + return describe('ensureUserCanReadMultipleProjects', function() { + beforeEach(function() { + this.AuthorizationManager.canUserReadProject = sinon.stub() + this.AuthorizationMiddleware.redirectToRestricted = sinon.stub() + return (this.req.query = { project_ids: 'project1,project2' }) + }) + + describe('with logged in user', function() { + beforeEach(function() { + return this.AuthenticationController.getLoggedInUserId.returns( + this.user_id + ) + }) + + describe('when user has permission to access all projects', function() { + beforeEach(function() { + this.AuthorizationManager.canUserReadProject + .withArgs(this.user_id, 'project1', this.token) + .yields(null, true) + return this.AuthorizationManager.canUserReadProject + .withArgs(this.user_id, 'project2', this.token) + .yields(null, true) + }) + + return it('should return next', function() { + this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( + this.req, + this.res, + this.next + ) + return this.next.called.should.equal(true) + }) + }) + + return describe("when user doesn't have permission to access one of the projects", function() { + beforeEach(function() { + this.AuthorizationManager.canUserReadProject + .withArgs(this.user_id, 'project1', this.token) + .yields(null, true) + return this.AuthorizationManager.canUserReadProject + .withArgs(this.user_id, 'project2', this.token) + .yields(null, false) + }) + + return it('should redirect to redirectToRestricted', function() { + this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( + this.req, + this.res, + this.next + ) + this.next.called.should.equal(false) + return this.AuthorizationMiddleware.redirectToRestricted + .calledWith(this.req, this.res, this.next) + .should.equal(true) + }) + }) + }) + + return describe('with anonymous user', () => + describe('when user has permission', function() { + describe('when user has permission to access all projects', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId.returns(null) + this.AuthorizationManager.canUserReadProject + .withArgs(null, 'project1', this.token) + .yields(null, true) + return this.AuthorizationManager.canUserReadProject + .withArgs(null, 'project2', this.token) + .yields(null, true) + }) + + return it('should return next', function() { + this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( + this.req, + this.res, + this.next + ) + return this.next.called.should.equal(true) + }) + }) + + return describe("when user doesn't have permission to access one of the projects", function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId.returns(null) + this.AuthorizationManager.canUserReadProject + .withArgs(null, 'project1', this.token) + .yields(null, true) + return this.AuthorizationManager.canUserReadProject + .withArgs(null, 'project2', this.token) + .yields(null, false) + }) + + return it('should redirect to redirectToRestricted', function() { + this.AuthorizationMiddleware.ensureUserCanReadMultipleProjects( + this.req, + this.res, + this.next + ) + this.next.called.should.equal(false) + return this.AuthorizationMiddleware.redirectToRestricted + .calledWith(this.req, this.res, this.next) + .should.equal(true) + }) + }) + })) + }) +}) diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramControllerTests.js b/services/web/test/unit/src/BetaProgram/BetaProgramControllerTests.js new file mode 100644 index 0000000000..ee7f37e670 --- /dev/null +++ b/services/web/test/unit/src/BetaProgram/BetaProgramControllerTests.js @@ -0,0 +1,193 @@ +/* eslint-disable + max-len, + mocha/no-identical-title, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/BetaProgram/BetaProgramController' +) +const { expect } = require('chai') + +describe('BetaProgramController', function() { + beforeEach(function() { + this.user = { + _id: (this.user_id = 'a_simple_id'), + email: 'user@example.com', + features: {}, + betaProgram: false + } + this.req = { + query: {}, + session: { + user: this.user + } + } + this.BetaProgramController = SandboxedModule.require(modulePath, { + requires: { + './BetaProgramHandler': (this.BetaProgramHandler = { + optIn: sinon.stub(), + optOut: sinon.stub() + }), + '../User/UserGetter': (this.UserGetter = { + getUser: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + languages: {} + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub(), + error: sinon.stub() + }), + '../Authentication/AuthenticationController': (this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user._id) + }) + } + }) + this.res = { + send: sinon.stub(), + redirect: sinon.stub(), + render: sinon.stub() + } + return (this.next = sinon.stub()) + }) + + describe('optIn', function() { + beforeEach(function() { + return this.BetaProgramHandler.optIn.callsArgWith(1, null) + }) + + it("should redirect to '/beta/participate'", function() { + this.BetaProgramController.optIn(this.req, this.res, this.next) + this.res.redirect.callCount.should.equal(1) + return this.res.redirect.firstCall.args[0].should.equal( + '/beta/participate' + ) + }) + + it('should not call next with an error', function() { + this.BetaProgramController.optIn(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + it('should not call next with an error', function() { + this.BetaProgramController.optIn(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + it('should call BetaProgramHandler.optIn', function() { + this.BetaProgramController.optIn(this.req, this.res, this.next) + return this.BetaProgramHandler.optIn.callCount.should.equal(1) + }) + + return describe('when BetaProgramHandler.opIn produces an error', function() { + beforeEach(function() { + return this.BetaProgramHandler.optIn.callsArgWith(1, new Error('woops')) + }) + + it("should not redirect to '/beta/participate'", function() { + this.BetaProgramController.optIn(this.req, this.res, this.next) + return this.res.redirect.callCount.should.equal(0) + }) + + return it('should produce an error', function() { + this.BetaProgramController.optIn(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return this.next.firstCall.args[0].should.be.instanceof(Error) + }) + }) + }) + + describe('optOut', function() { + beforeEach(function() { + return this.BetaProgramHandler.optOut.callsArgWith(1, null) + }) + + it("should redirect to '/beta/participate'", function() { + this.BetaProgramController.optOut(this.req, this.res, this.next) + this.res.redirect.callCount.should.equal(1) + return this.res.redirect.firstCall.args[0].should.equal( + '/beta/participate' + ) + }) + + it('should not call next with an error', function() { + this.BetaProgramController.optOut(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + it('should not call next with an error', function() { + this.BetaProgramController.optOut(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + it('should call BetaProgramHandler.optOut', function() { + this.BetaProgramController.optOut(this.req, this.res, this.next) + return this.BetaProgramHandler.optOut.callCount.should.equal(1) + }) + + return describe('when BetaProgramHandler.optOut produces an error', function() { + beforeEach(function() { + return this.BetaProgramHandler.optOut.callsArgWith( + 1, + new Error('woops') + ) + }) + + it("should not redirect to '/beta/participate'", function() { + this.BetaProgramController.optOut(this.req, this.res, this.next) + return this.res.redirect.callCount.should.equal(0) + }) + + return it('should produce an error', function() { + this.BetaProgramController.optOut(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return this.next.firstCall.args[0].should.be.instanceof(Error) + }) + }) + }) + + return describe('optInPage', function() { + beforeEach(function() { + return this.UserGetter.getUser.callsArgWith(1, null, this.user) + }) + + it('should render the opt-in page', function() { + this.BetaProgramController.optInPage(this.req, this.res, this.next) + this.res.render.callCount.should.equal(1) + const { args } = this.res.render.firstCall + return args[0].should.equal('beta_program/opt_in') + }) + + return describe('when UserGetter.getUser produces an error', function() { + beforeEach(function() { + return this.UserGetter.getUser.callsArgWith(1, new Error('woops')) + }) + + it('should not render the opt-in page', function() { + this.BetaProgramController.optInPage(this.req, this.res, this.next) + return this.res.render.callCount.should.equal(0) + }) + + return it('should produce an error', function() { + this.BetaProgramController.optInPage(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return this.next.firstCall.args[0].should.be.instanceof(Error) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramHandlerTests.js b/services/web/test/unit/src/BetaProgram/BetaProgramHandlerTests.js new file mode 100644 index 0000000000..657b5a57b1 --- /dev/null +++ b/services/web/test/unit/src/BetaProgram/BetaProgramHandlerTests.js @@ -0,0 +1,142 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/BetaProgram/BetaProgramHandler' +) +const sinon = require('sinon') +const { expect } = require('chai') + +describe('BetaProgramHandler', function() { + beforeEach(function() { + this.user_id = 'some_id' + this.user = { + _id: this.user_id, + email: 'user@example.com', + features: {}, + betaProgram: false, + save: sinon.stub().callsArgWith(0, null) + } + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: { + findById: sinon.stub().callsArgWith(1, null, this.user) + } + }, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }), + 'metrics-sharelatex': (this.logger = { + inc: sinon.stub() + }) + } + })) + }) + + describe('optIn', function() { + beforeEach(function() { + this.user.betaProgram = false + return (this.call = callback => { + return this.handler.optIn(this.user_id, callback) + }) + }) + + it('should set betaProgram = true on user object', function(done) { + return this.call(err => { + this.user.betaProgram.should.equal(true) + return done() + }) + }) + + it('should call user.save', function(done) { + return this.call(err => { + this.user.save.callCount.should.equal(1) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.equal(null) + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + return describe('when user.save produces an error', function() { + beforeEach(function() { + return this.user.save.callsArgWith(0, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + return describe('optOut', function() { + beforeEach(function() { + this.user.betaProgram = true + return (this.call = callback => { + return this.handler.optOut(this.user_id, callback) + }) + }) + + it('should set betaProgram = true on user object', function(done) { + return this.call(err => { + this.user.betaProgram.should.equal(false) + return done() + }) + }) + + it('should call user.save', function(done) { + return this.call(err => { + this.user.save.callCount.should.equal(1) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.equal(null) + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + return describe('when user.save produces an error', function() { + beforeEach(function() { + return this.user.save.callsArgWith(0, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Blog/BlogControllerTests.js b/services/web/test/unit/src/Blog/BlogControllerTests.js new file mode 100644 index 0000000000..a81bf06c2a --- /dev/null +++ b/services/web/test/unit/src/Blog/BlogControllerTests.js @@ -0,0 +1,101 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Blog/BlogController' +) +const { expect } = require('chai') + +describe('BlogController', function() { + beforeEach(function() { + this.settings = { + apis: { + blog: { + url: 'http://blog.sharelatex.env' + } + } + } + this.request = { get: sinon.stub() } + this.ErrorController = {} + this.BlogController = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {} + }, + '../Errors/ErrorController': this.ErrorController, + request: this.request + } + }) + + this.req = {} + return (this.res = {}) + }) + + describe('getPage', function() { + it('should get the data from the blog api', function(done) { + this.req.url = '/blog/something.html' + const body = { stuff: 'here' } + + this.request.get.callsArgWith(1, null, null, JSON.stringify(body)) + this.res.render = (view, data) => { + this.request.get.calledWith( + `${this.settings.apis.blog.url}${this.req.url}` + ) + view.should.equal('blog/blog_holder') + assert.deepEqual(body, data) + return done() + } + + return this.BlogController.getPage(this.req, this.res) + }) + + it('should send to the error controller if the blog responds 404', function(done) { + this.req.url = '/blog/something.html' + this.request.get.callsArgWith(1, null, { statusCode: 404 }) + + this.ErrorController.notFound = (req, res) => { + assert.deepEqual(req, this.req) + assert.deepEqual(res, this.res) + return done() + } + + return this.BlogController.getPage(this.req, this.res) + }) + + return it('should proxy the image urls', function(done) { + this.BlogController._directProxy = sinon.stub() + this.req.url = '/something.png' + this.BlogController.getPage(this.req, this.res) + this.BlogController._directProxy + .calledWith(`${this.settings.apis.blog.url}${this.req.url}`, this.res) + .should.equal(true) + return done() + }) + }) + + return describe('getIndexPage', () => + it('should change the url and send it to getPage', function(done) { + this.req.url = '/blog' + this.BlogController.getPage = function(req, res) { + req.url.should.equal('/blog/index.html') + return done() + } + return this.BlogController.getIndexPage(this.req, this.res) + })) +}) diff --git a/services/web/test/unit/src/BrandVariations/BrandVariationsHandlerTests.js b/services/web/test/unit/src/BrandVariations/BrandVariationsHandlerTests.js new file mode 100644 index 0000000000..525a68b758 --- /dev/null +++ b/services/web/test/unit/src/BrandVariations/BrandVariationsHandlerTests.js @@ -0,0 +1,117 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +;({ expect } = require('chai')) +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/BrandVariations/BrandVariationsHandler' +) + +describe('BrandVariationsHandler', function() { + beforeEach(function() { + this.settings = { + apis: { + v1: { + url: 'http://overleaf.example.com' + } + } + } + this.logger = { + err() {}, + log() {} + } + this.V1Api = { request: sinon.stub() } + this.BrandVariationsHandler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': this.logger, + '../V1/V1Api': this.V1Api + } + }) + return (this.mockedBrandVariationDetails = { + id: '12', + active: true, + brand_name: 'The journal', + logo_url: 'http://my.cdn.tld/journal-logo.png', + journal_cover_url: 'http://my.cdn.tld/journal-cover.jpg', + home_url: 'http://www.thejournal.com/', + publish_menu_link_html: 'Submit your paper to the The Journal' + }) + }) + + return describe('getBrandVariationById', function() { + it('should call the callback with an error when the branding variation id is not provided', function(done) { + return this.BrandVariationsHandler.getBrandVariationById( + null, + (err, brandVariationDetails) => { + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + + it('should call the callback with an error when the request errors', function(done) { + this.V1Api.request.callsArgWith(1, new Error()) + return this.BrandVariationsHandler.getBrandVariationById( + '12', + (err, brandVariationDetails) => { + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + + it('should call the callback with branding details when request succeeds', function(done) { + this.V1Api.request.callsArgWith( + 1, + null, + { statusCode: 200 }, + this.mockedBrandVariationDetails + ) + return this.BrandVariationsHandler.getBrandVariationById( + '12', + (err, brandVariationDetails) => { + expect(err).to.not.exist + expect(brandVariationDetails).to.deep.equal( + this.mockedBrandVariationDetails + ) + return done() + } + ) + }) + + return it('should transform relative URLs in v1 absolute ones', function(done) { + this.mockedBrandVariationDetails.logo_url = '/journal-logo.png' + this.V1Api.request.callsArgWith( + 1, + null, + { statusCode: 200 }, + this.mockedBrandVariationDetails + ) + return this.BrandVariationsHandler.getBrandVariationById( + '12', + (err, brandVariationDetails) => { + expect( + brandVariationDetails.logo_url.startsWith(this.settings.apis.v1.url) + ).to.be.true + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Chat/ChatApiHandlerTests.js b/services/web/test/unit/src/Chat/ChatApiHandlerTests.js new file mode 100644 index 0000000000..41d77c2fa6 --- /dev/null +++ b/services/web/test/unit/src/Chat/ChatApiHandlerTests.js @@ -0,0 +1,156 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Chat/ChatApiHandler' +) +const { expect } = require('chai') + +describe('ChatApiHandler', function() { + beforeEach(function() { + this.settings = { + apis: { + chat: { + internal_url: 'chat.sharelatex.env' + } + } + } + this.request = sinon.stub() + this.ChatApiHandler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { log: sinon.stub(), error: sinon.stub() }, + request: this.request + } + }) + this.project_id = '3213213kl12j' + this.user_id = '2k3jlkjs9' + this.content = 'my message here' + return (this.callback = sinon.stub()) + }) + + describe('sendGlobalMessage', function() { + describe('successfully', function() { + beforeEach(function() { + this.message = { mock: 'message' } + this.request.callsArgWith(1, null, { statusCode: 200 }, this.message) + return this.ChatApiHandler.sendGlobalMessage( + this.project_id, + this.user_id, + this.content, + this.callback + ) + }) + + it('should post the data to the chat api', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.chat.internal_url}/project/${ + this.project_id + }/messages`, + method: 'POST', + json: { + content: this.content, + user_id: this.user_id + } + }) + .should.equal(true) + }) + + return it('should return the message from the post', function() { + return this.callback.calledWith(null, this.message).should.equal(true) + }) + }) + + return describe('with a non-success status code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }) + return this.ChatApiHandler.sendGlobalMessage( + this.project_id, + this.user_id, + this.content, + this.callback + ) + }) + + return it('should return an error', function() { + const error = new Error() + error.statusCode = 500 + return this.callback.calledWith(error).should.equal(true) + }) + }) + }) + + return describe('getGlobalMessages', function() { + beforeEach(function() { + this.messages = [{ mock: 'message' }] + this.limit = 30 + return (this.before = '1234') + }) + + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 200 }, this.messages) + return this.ChatApiHandler.getGlobalMessages( + this.project_id, + this.limit, + this.before, + this.callback + ) + }) + + it('should make get request for room to chat api', function() { + return this.request + .calledWith({ + method: 'GET', + url: `${this.settings.apis.chat.internal_url}/project/${ + this.project_id + }/messages`, + qs: { + limit: this.limit, + before: this.before + }, + json: true + }) + .should.equal(true) + }) + + return it('should return the messages from the request', function() { + return this.callback.calledWith(null, this.messages).should.equal(true) + }) + }) + + return describe('with failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, null) + return this.ChatApiHandler.getGlobalMessages( + this.project_id, + this.limit, + this.before, + this.callback + ) + }) + + return it('should return an error', function() { + const error = new Error() + error.statusCode = 500 + return this.callback.calledWith(error).should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Chat/ChatControllerTests.js b/services/web/test/unit/src/Chat/ChatControllerTests.js new file mode 100644 index 0000000000..17887e2640 --- /dev/null +++ b/services/web/test/unit/src/Chat/ChatControllerTests.js @@ -0,0 +1,235 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Chat/ChatController' +) +const { expect } = require('chai') + +describe('ChatController', function() { + beforeEach(function() { + this.user_id = 'mock-user-id' + this.settings = {} + this.ChatApiHandler = {} + this.EditorRealTimeController = { emitToRoom: sinon.stub() } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user_id) + } + this.ChatController = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {} + }, + './ChatApiHandler': this.ChatApiHandler, + '../Editor/EditorRealTimeController': this.EditorRealTimeController, + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../User/UserInfoManager': (this.UserInfoManager = {}), + '../User/UserInfoController': (this.UserInfoController = {}), + '../Comments/CommentsController': (this.CommentsController = {}) + } + }) + this.req = { + params: { + project_id: this.project_id + } + } + return (this.res = { + json: sinon.stub(), + send: sinon.stub() + }) + }) + + describe('sendMessage', function() { + beforeEach(function() { + this.req.body = { content: (this.content = 'message-content') } + this.UserInfoManager.getPersonalInfo = sinon + .stub() + .yields(null, (this.user = { unformatted: 'user' })) + this.UserInfoController.formatPersonalInfo = sinon + .stub() + .returns((this.formatted_user = { formatted: 'user' })) + this.ChatApiHandler.sendGlobalMessage = sinon + .stub() + .yields( + null, + (this.message = { mock: 'message', user_id: this.user_id }) + ) + return this.ChatController.sendMessage(this.req, this.res) + }) + + it('should look up the user', function() { + return this.UserInfoManager.getPersonalInfo + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should format and inject the user into the message', function() { + this.UserInfoController.formatPersonalInfo + .calledWith(this.user) + .should.equal(true) + return this.message.user.should.deep.equal(this.formatted_user) + }) + + it('should tell the chat handler about the message', function() { + return this.ChatApiHandler.sendGlobalMessage + .calledWith(this.project_id, this.user_id, this.content) + .should.equal(true) + }) + + it('should tell the editor real time controller about the update with the data from the chat handler', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'new-chat-message', this.message) + .should.equal(true) + }) + + return it('should return a 204 status code', function() { + return this.res.send.calledWith(204).should.equal(true) + }) + }) + + describe('getMessages', function() { + beforeEach(function() { + this.req.query = { + limit: (this.limit = '30'), + before: (this.before = '12345') + } + this.ChatController._injectUserInfoIntoThreads = sinon.stub().yields() + this.ChatApiHandler.getGlobalMessages = sinon + .stub() + .yields(null, (this.messages = ['mock', 'messages'])) + return this.ChatController.getMessages(this.req, this.res) + }) + + it('should ask the chat handler about the request', function() { + return this.ChatApiHandler.getGlobalMessages + .calledWith(this.project_id, this.limit, this.before) + .should.equal(true) + }) + + return it('should return the messages', function() { + return this.res.json.calledWith(this.messages).should.equal(true) + }) + }) + + return describe('_injectUserInfoIntoThreads', function() { + beforeEach(function() { + this.users = { + user_id_1: { + mock: 'user_1' + }, + user_id_2: { + mock: 'user_2' + } + } + this.UserInfoManager.getPersonalInfo = (user_id, callback) => { + return callback(null, this.users[user_id]) + } + sinon.spy(this.UserInfoManager, 'getPersonalInfo') + return (this.UserInfoController.formatPersonalInfo = user => ({ + formatted: user['mock'] + })) + }) + + it('should inject a user object into messaged and resolved data', function(done) { + return this.ChatController._injectUserInfoIntoThreads( + { + thread1: { + resolved: true, + resolved_by_user_id: 'user_id_1', + messages: [ + { + user_id: 'user_id_1', + content: 'foo' + }, + { + user_id: 'user_id_2', + content: 'bar' + } + ] + }, + thread2: { + messages: [ + { + user_id: 'user_id_1', + content: 'baz' + } + ] + } + }, + function(error, threads) { + expect(threads).to.deep.equal({ + thread1: { + resolved: true, + resolved_by_user_id: 'user_id_1', + resolved_by_user: { formatted: 'user_1' }, + messages: [ + { + user_id: 'user_id_1', + user: { formatted: 'user_1' }, + content: 'foo' + }, + { + user_id: 'user_id_2', + user: { formatted: 'user_2' }, + content: 'bar' + } + ] + }, + thread2: { + messages: [ + { + user_id: 'user_id_1', + user: { formatted: 'user_1' }, + content: 'baz' + } + ] + } + }) + return done() + } + ) + }) + + return it('should only need to look up each user once', function(done) { + return this.ChatController._injectUserInfoIntoThreads( + [ + { + messages: [ + { + user_id: 'user_id_1', + content: 'foo' + }, + { + user_id: 'user_id_1', + content: 'bar' + } + ] + } + ], + (error, threads) => { + this.UserInfoManager.getPersonalInfo.calledOnce.should.equal(true) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js new file mode 100644 index 0000000000..3fd46c4ef0 --- /dev/null +++ b/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.js @@ -0,0 +1,175 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Collaborators/CollaboratorsController.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const { ObjectId } = require('mongojs') + +describe('CollaboratorsController', function() { + beforeEach(function() { + this.CollaboratorsController = SandboxedModule.require(modulePath, { + requires: { + '../Project/ProjectGetter': (this.ProjectGetter = {}), + './CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Editor/EditorRealTimeController': (this.EditorRealTimeController = {}), + '../Subscription/LimitationsManager': (this.LimitationsManager = {}), + '../Project/ProjectEditorHandler': (this.ProjectEditorHandler = {}), + '../User/UserGetter': (this.UserGetter = {}), + 'logger-sharelatex': (this.logger = { + err: sinon.stub(), + erro: sinon.stub(), + log: sinon.stub() + }) + } + }) + this.res = new MockResponse() + this.req = new MockRequest() + + this.project_id = 'project-id-123' + return (this.callback = sinon.stub()) + }) + + describe('removeUserFromProject', function() { + beforeEach(function() { + this.req.params = { + Project_id: (this.project_id = 'project-id-123'), + user_id: (this.user_id = 'user-id-123') + } + this.res.sendStatus = sinon.stub() + this.EditorRealTimeController.emitToRoom = sinon.stub() + this.CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) + return this.CollaboratorsController.removeUserFromProject( + this.req, + this.res + ) + }) + + it('should from the user from the project', function() { + return this.CollaboratorsHandler.removeUserFromProject + .calledWith(this.project_id, this.user_id) + .should.equal(true) + }) + + it('should emit a userRemovedFromProject event to the proejct', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'userRemovedFromProject', this.user_id) + .should.equal(true) + }) + + it('should send the back a success response', function() { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + + return it('should have called emitToRoom', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:membership:changed') + .should.equal(true) + }) + }) + + describe('removeSelfFromProject', function() { + beforeEach(function() { + this.req.session = { user: { _id: (this.user_id = 'user-id-123') } } + this.req.params = { Project_id: this.project_id } + this.res.sendStatus = sinon.stub() + this.EditorRealTimeController.emitToRoom = sinon.stub() + this.CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) + return this.CollaboratorsController.removeSelfFromProject( + this.req, + this.res + ) + }) + + it('should remove the logged in user from the project', function() { + return this.CollaboratorsHandler.removeUserFromProject + .calledWith(this.project_id, this.user_id) + .should.equal(true) + }) + + it('should emit a userRemovedFromProject event to the proejct', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'userRemovedFromProject', this.user_id) + .should.equal(true) + }) + + return it('should return a success code', function() { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + }) + + return describe('getAllMembers', function() { + beforeEach(function() { + this.req.session = { user: { _id: (this.user_id = 'user-id-123') } } + this.req.params = { Project_id: this.project_id } + this.res.json = sinon.stub() + this.next = sinon.stub() + this.members = [{ a: 1 }] + this.CollaboratorsHandler.getAllInvitedMembers = sinon + .stub() + .callsArgWith(1, null, this.members) + return this.CollaboratorsController.getAllMembers( + this.req, + this.res, + this.next + ) + }) + + it('should not produce an error', function() { + return this.next.callCount.should.equal(0) + }) + + it('should produce a json response', function() { + this.res.json.callCount.should.equal(1) + return this.res.json + .calledWith({ members: this.members }) + .should.equal(true) + }) + + it('should call CollaboratorsHandler.getAllMembers', function() { + return this.CollaboratorsHandler.getAllInvitedMembers.callCount.should.equal( + 1 + ) + }) + + return describe('when CollaboratorsHandler.getAllInvitedMembers produces an error', function() { + beforeEach(function() { + this.res.json = sinon.stub() + this.next = sinon.stub() + this.CollaboratorsHandler.getAllInvitedMembers = sinon + .stub() + .callsArgWith(1, new Error('woops')) + return this.CollaboratorsController.getAllMembers( + this.req, + this.res, + this.next + ) + }) + + it('should produce an error', function() { + this.next.callCount.should.equal(1) + return this.next.firstCall.args[0].should.be.instanceof(Error) + }) + + return it('should not produce a json response', function() { + return this.res.json.callCount.should.equal(0) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js new file mode 100644 index 0000000000..d7e7a3624b --- /dev/null +++ b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js @@ -0,0 +1,1042 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Collaborators/CollaboratorsHandler' +) +const { expect } = require('chai') +const Errors = require('../../../../app/src/Features/Errors/Errors.js') +const { ObjectId } = require('mongojs') + +describe('CollaboratorsHandler', function() { + beforeEach(function() { + this.CollaboratorHandler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }), + '../User/UserCreator': (this.UserCreator = {}), + '../User/UserGetter': (this.UserGetter = {}), + '../Contacts/ContactManager': (this.ContactManager = {}), + '../../models/Project': { + Project: (this.Project = {}) + }, + '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + './CollaboratorsEmailHandler': (this.CollaboratorsEmailHandler = {}), + '../Errors/Errors': Errors, + '../Project/ProjectEditorHandler': (this.ProjectEditorHandler = {}) + } + }) + + this.project_id = 'mock-project-id' + this.user_id = 'mock-user-id' + this.adding_user_id = 'adding-user-id' + this.email = 'joe@sharelatex.com' + return (this.callback = sinon.stub()) + }) + + describe('getMemberIdsWithPrivilegeLevels', function() { + describe('with project', function() { + beforeEach(function() { + this.ProjectGetter.getProject = sinon.stub() + this.ProjectGetter.getProject + .withArgs(this.project_id, { + owner_ref: 1, + collaberator_refs: 1, + readOnly_refs: 1, + tokenAccessReadOnly_refs: 1, + tokenAccessReadAndWrite_refs: 1, + publicAccesLevel: 1 + }) + .yields( + null, + (this.project = { + owner_ref: ['owner-ref'], + readOnly_refs: ['read-only-ref-1', 'read-only-ref-2'], + collaberator_refs: ['read-write-ref-1', 'read-write-ref-2'] + }) + ) + return this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels( + this.project_id, + this.callback + ) + }) + + return it('should return an array of member ids with their privilege levels', function() { + return this.callback + .calledWith(null, [ + { id: 'owner-ref', privilegeLevel: 'owner', source: 'owner' }, + { + id: 'read-write-ref-1', + privilegeLevel: 'readAndWrite', + source: 'invite' + }, + { + id: 'read-write-ref-2', + privilegeLevel: 'readAndWrite', + source: 'invite' + }, + { + id: 'read-only-ref-1', + privilegeLevel: 'readOnly', + source: 'invite' + }, + { + id: 'read-only-ref-2', + privilegeLevel: 'readOnly', + source: 'invite' + } + ]) + .should.equal(true) + }) + }) + + return describe('with a missing project', function() { + beforeEach(function() { + return (this.ProjectGetter.getProject = sinon.stub().yields(null, null)) + }) + + return it('should return a NotFoundError', function(done) { + return this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels( + this.project_id, + function(error) { + error.should.be.instanceof(Errors.NotFoundError) + return done() + } + ) + }) + }) + }) + + describe('getMemberIds', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [ + { id: 'member-id-1', source: 'invite' }, + { id: 'member-id-2', source: 'token' } + ]) + return this.CollaboratorHandler.getMemberIds( + this.project_id, + this.callback + ) + }) + + return it('should return the ids', function() { + return this.callback + .calledWith(null, ['member-id-1', 'member-id-2']) + .should.equal(true) + }) + }) + + describe('getInvitedMemberIds', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [ + { id: 'member-id-1', source: 'invite' }, + { id: 'member-id-2', source: 'token' } + ]) + return this.CollaboratorHandler.getInvitedMemberIds( + this.project_id, + this.callback + ) + }) + + return it('should return the invited ids', function() { + return this.callback.calledWith(null, ['member-id-1']).should.equal(true) + }) + }) + + describe('getMembersWithPrivilegeLevels', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [ + { + id: 'read-only-ref-1', + privilegeLevel: 'readOnly', + source: 'token' + }, + { + id: 'read-only-ref-2', + privilegeLevel: 'readOnly', + source: 'invite' + }, + { + id: 'read-write-ref-1', + privilegeLevel: 'readAndWrite', + source: 'token' + }, + { + id: 'read-write-ref-2', + privilegeLevel: 'readAndWrite', + source: 'invite' + }, + { + id: 'doesnt-exist', + privilegeLevel: 'readAndWrite', + source: 'invite' + } + ]) + this.UserGetter.getUserOrUserStubById = sinon.stub() + this.UserGetter.getUserOrUserStubById + .withArgs('read-only-ref-1') + .yields(null, { _id: 'read-only-ref-1' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-only-ref-2') + .yields(null, { _id: 'read-only-ref-2' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-write-ref-1') + .yields(null, { _id: 'read-write-ref-1' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-write-ref-2') + .yields(null, { _id: 'read-write-ref-2' }) + this.UserGetter.getUserOrUserStubById + .withArgs('doesnt-exist') + .yields(null, null) + return this.CollaboratorHandler.getMembersWithPrivilegeLevels( + this.project_id, + this.callback + ) + }) + + return it('should return an array of members with their privilege levels', function() { + return this.callback + .calledWith(null, [ + { user: { _id: 'read-only-ref-1' }, privilegeLevel: 'readOnly' }, + { user: { _id: 'read-only-ref-2' }, privilegeLevel: 'readOnly' }, + { user: { _id: 'read-write-ref-1' }, privilegeLevel: 'readAndWrite' }, + { user: { _id: 'read-write-ref-2' }, privilegeLevel: 'readAndWrite' } + ]) + .should.equal(true) + }) + }) + + describe('getInvitedMembersWithPrivilegeLevels', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [ + { + id: 'read-only-ref-1', + privilegeLevel: 'readOnly', + source: 'token' + }, + { + id: 'read-only-ref-2', + privilegeLevel: 'readOnly', + source: 'invite' + }, + { + id: 'read-write-ref-1', + privilegeLevel: 'readAndWrite', + source: 'token' + }, + { + id: 'read-write-ref-2', + privilegeLevel: 'readAndWrite', + source: 'invite' + }, + { + id: 'doesnt-exist', + privilegeLevel: 'readAndWrite', + source: 'invite' + } + ]) + this.UserGetter.getUserOrUserStubById = sinon.stub() + this.UserGetter.getUserOrUserStubById + .withArgs('read-only-ref-1') + .yields(null, { _id: 'read-only-ref-1' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-only-ref-2') + .yields(null, { _id: 'read-only-ref-2' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-write-ref-1') + .yields(null, { _id: 'read-write-ref-1' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-write-ref-2') + .yields(null, { _id: 'read-write-ref-2' }) + this.UserGetter.getUserOrUserStubById + .withArgs('doesnt-exist') + .yields(null, null) + return this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels( + this.project_id, + this.callback + ) + }) + + return it('should return an array of invited members with their privilege levels', function() { + return this.callback + .calledWith(null, [ + { user: { _id: 'read-only-ref-2' }, privilegeLevel: 'readOnly' }, + { user: { _id: 'read-write-ref-2' }, privilegeLevel: 'readAndWrite' } + ]) + .should.equal(true) + }) + }) + + describe('getTokenMembersWithPrivilegeLevels', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [ + { + id: 'read-only-ref-1', + privilegeLevel: 'readOnly', + source: 'token' + }, + { + id: 'read-only-ref-2', + privilegeLevel: 'readOnly', + source: 'invite' + }, + { + id: 'read-write-ref-1', + privilegeLevel: 'readAndWrite', + source: 'token' + }, + { + id: 'read-write-ref-2', + privilegeLevel: 'readAndWrite', + source: 'invite' + }, + { + id: 'doesnt-exist', + privilegeLevel: 'readAndWrite', + source: 'invite' + } + ]) + this.UserGetter.getUserOrUserStubById = sinon.stub() + this.UserGetter.getUserOrUserStubById + .withArgs('read-only-ref-1') + .yields(null, { _id: 'read-only-ref-1' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-only-ref-2') + .yields(null, { _id: 'read-only-ref-2' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-write-ref-1') + .yields(null, { _id: 'read-write-ref-1' }) + this.UserGetter.getUserOrUserStubById + .withArgs('read-write-ref-2') + .yields(null, { _id: 'read-write-ref-2' }) + this.UserGetter.getUserOrUserStubById + .withArgs('doesnt-exist') + .yields(null, null) + return this.CollaboratorHandler.getTokenMembersWithPrivilegeLevels( + this.project_id, + this.callback + ) + }) + + return it('should return an array of token members with their privilege levels', function() { + return this.callback + .calledWith(null, [ + { user: { _id: 'read-only-ref-1' }, privilegeLevel: 'readOnly' }, + { user: { _id: 'read-write-ref-1' }, privilegeLevel: 'readAndWrite' } + ]) + .should.equal(true) + }) + }) + + describe('getMemberIdPrivilegeLevel', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + return this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [ + { id: 'member-id-1', privilegeLevel: 'readAndWrite' }, + { id: 'member-id-2', privilegeLevel: 'readOnly' } + ]) + }) + + it('should return the privilege level if it exists', function(done) { + return this.CollaboratorHandler.getMemberIdPrivilegeLevel( + 'member-id-2', + this.project_id, + function(error, level) { + expect(level).to.equal('readOnly') + return done() + } + ) + }) + + return it('should return false if the member has no privilege level', function(done) { + return this.CollaboratorHandler.getMemberIdPrivilegeLevel( + 'member-id-3', + this.project_id, + function(error, level) { + expect(level).to.equal(false) + return done() + } + ) + }) + }) + + describe('isUserInvitedMemberOfProject', function() { + beforeEach(function() { + return (this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()) + }) + + describe('when user is a member of the project', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [ + { + id: 'not-the-user', + privilegeLevel: 'readOnly', + source: 'invite' + }, + { + id: this.user_id, + privilegeLevel: 'readAndWrite', + source: 'invite' + } + ]) + return this.CollaboratorHandler.isUserInvitedMemberOfProject( + this.user_id, + this.project_id, + this.callback + ) + }) + + return it('should return true and the privilegeLevel', function() { + return this.callback + .calledWith(null, true, 'readAndWrite') + .should.equal(true) + }) + }) + + return describe('when user is not a member of the project', function() { + beforeEach(function() { + this.CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(this.project_id) + .yields(null, [{ id: 'not-the-user', privilegeLevel: 'readOnly' }]) + return this.CollaboratorHandler.isUserInvitedMemberOfProject( + this.user_id, + this.project_id, + this.callback + ) + }) + + return it('should return false', function() { + return this.callback.calledWith(null, false, null).should.equal(true) + }) + }) + }) + + describe('getProjectsUserIsMemberOf', function() { + beforeEach(function() { + this.fields = 'mock fields' + this.Project.find = sinon.stub() + this.Project.find + .withArgs({ collaberator_refs: this.user_id }, this.fields) + .yields(null, [ + 'mock-read-write-project-1', + 'mock-read-write-project-2' + ]) + this.Project.find + .withArgs({ readOnly_refs: this.user_id }, this.fields) + .yields(null, ['mock-read-only-project-1', 'mock-read-only-project-2']) + this.Project.find + .withArgs( + { + tokenAccessReadAndWrite_refs: this.user_id, + publicAccesLevel: 'tokenBased' + }, + this.fields + ) + .yields(null, [ + 'mock-token-read-write-project-1', + 'mock-token-read-write-project-2' + ]) + this.Project.find + .withArgs( + { + tokenAccessReadOnly_refs: this.user_id, + publicAccesLevel: 'tokenBased' + }, + this.fields + ) + .yields(null, [ + 'mock-token-read-only-project-1', + 'mock-token-read-only-project-2' + ]) + return this.CollaboratorHandler.getProjectsUserIsMemberOf( + this.user_id, + this.fields, + this.callback + ) + }) + + return it('should call the callback with the projects', function() { + return this.callback + .calledWith(null, { + readAndWrite: [ + 'mock-read-write-project-1', + 'mock-read-write-project-2' + ], + readOnly: ['mock-read-only-project-1', 'mock-read-only-project-2'], + tokenReadAndWrite: [ + 'mock-token-read-write-project-1', + 'mock-token-read-write-project-2' + ], + tokenReadOnly: [ + 'mock-token-read-only-project-1', + 'mock-token-read-only-project-2' + ] + }) + .should.equal(true) + }) + }) + + describe('removeUserFromProject', function() { + beforeEach(function() { + this.Project.update = sinon.stub().callsArg(2) + return this.CollaboratorHandler.removeUserFromProject( + this.project_id, + this.user_id, + this.callback + ) + }) + + return it('should remove the user from mongo', function() { + return this.Project.update + .calledWith( + { + _id: this.project_id + }, + { + $pull: { + collaberator_refs: this.user_id, + readOnly_refs: this.user_id, + tokenAccessReadOnly_refs: this.user_id, + tokenAccessReadAndWrite_refs: this.user_id + } + } + ) + .should.equal(true) + }) + }) + + describe('addUserToProject', function() { + beforeEach(function() { + this.Project.update = sinon.stub().callsArg(2) + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, (this.project = {})) + this.ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon + .stub() + .callsArg(1) + this.CollaboratorHandler.addEmailToProject = sinon + .stub() + .callsArgWith(4, null, this.user_id) + return (this.ContactManager.addContact = sinon.stub()) + }) + + describe('as readOnly', function() { + beforeEach(function() { + return this.CollaboratorHandler.addUserIdToProject( + this.project_id, + this.adding_user_id, + this.user_id, + 'readOnly', + this.callback + ) + }) + + it('should add the user to the readOnly_refs', function() { + return this.Project.update + .calledWith( + { + _id: this.project_id + }, + { + $addToSet: { readOnly_refs: this.user_id } + } + ) + .should.equal(true) + }) + + it('should flush the project to the TPDS', function() { + return this.ProjectEntityHandler.flushProjectToThirdPartyDataStore + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should add the user as a contact for the adding user', function() { + return this.ContactManager.addContact + .calledWith(this.adding_user_id, this.user_id) + .should.equal(true) + }) + }) + + describe('as readAndWrite', function() { + beforeEach(function() { + return this.CollaboratorHandler.addUserIdToProject( + this.project_id, + this.adding_user_id, + this.user_id, + 'readAndWrite', + this.callback + ) + }) + + it('should add the user to the collaberator_refs', function() { + return this.Project.update + .calledWith( + { + _id: this.project_id + }, + { + $addToSet: { collaberator_refs: this.user_id } + } + ) + .should.equal(true) + }) + + return it('should flush the project to the TPDS', function() { + return this.ProjectEntityHandler.flushProjectToThirdPartyDataStore + .calledWith(this.project_id) + .should.equal(true) + }) + }) + + describe('with invalid privilegeLevel', function() { + beforeEach(function() { + return this.CollaboratorHandler.addUserIdToProject( + this.project_id, + this.adding_user_id, + this.user_id, + 'notValid', + this.callback + ) + }) + + return it('should call the callback with an error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + + describe('when user already exists as a collaborator', function() { + beforeEach(function() { + this.project.collaberator_refs = [this.user_id] + return this.CollaboratorHandler.addUserIdToProject( + this.project_id, + this.adding_user_id, + this.user_id, + 'readAndWrite', + this.callback + ) + }) + + return it('should not add the user again', function() { + return this.Project.update.called.should.equal(false) + }) + }) + + return describe('with null adding_user_id', function() { + beforeEach(function() { + return this.CollaboratorHandler.addUserIdToProject( + this.project_id, + null, + this.user_id, + 'readAndWrite', + this.callback + ) + }) + + return it('should not add the adding user as a contact', function() { + return this.ContactManager.addContact.called.should.equal(false) + }) + }) + }) + + describe('removeUserFromAllProjects', function() { + beforeEach(function(done) { + this.CollaboratorHandler.getProjectsUserIsMemberOf = sinon.stub() + this.CollaboratorHandler.getProjectsUserIsMemberOf + .withArgs(this.user_id, { _id: 1 }) + .yields(null, { + readAndWrite: [ + { _id: 'read-and-write-0' }, + { _id: 'read-and-write-1' }, + null + ], + readOnly: [{ _id: 'read-only-0' }, { _id: 'read-only-1' }, null], + tokenReadAndWrite: [ + { _id: 'token-read-and-write-0' }, + { _id: 'token-read-and-write-1' }, + null + ], + tokenReadOnly: [ + { _id: 'token-read-only-0' }, + { _id: 'token-read-only-1' }, + null + ] + }) + this.CollaboratorHandler.removeUserFromProject = sinon.stub().yields() + return this.CollaboratorHandler.removeUserFromAllProjets( + this.user_id, + done + ) + }) + + return it('should remove the user from each project', function() { + const expectedProjects = [ + 'read-and-write-0', + 'read-and-write-1', + 'read-only-0', + 'read-only-1', + 'token-read-and-write-0', + 'token-read-and-write-1', + 'token-read-only-0', + 'token-read-only-1' + ] + return Array.from(expectedProjects).map(project_id => + this.CollaboratorHandler.removeUserFromProject + .calledWith(project_id, this.user_id) + .should.equal(true) + ) + }) + }) + + describe('getAllInvitedMembers', function() { + beforeEach(function() { + this.owning_user = { + _id: 'owner-id', + email: 'owner@example.com', + features: { a: 1 } + } + this.readwrite_user = { + _id: 'readwrite-id', + email: 'readwrite@example.com' + } + this.members = [ + { user: this.owning_user, privilegeLevel: 'owner' }, + { user: this.readwrite_user, privilegeLevel: 'readAndWrite' } + ] + this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels = sinon + .stub() + .callsArgWith(1, null, this.members) + this.ProjectEditorHandler.buildOwnerAndMembersViews = sinon + .stub() + .returns( + (this.views = { + owner: this.owning_user, + ownerFeatures: this.owning_user.features, + members: [ + { _id: this.readwrite_user._id, email: this.readwrite_user.email } + ] + }) + ) + this.callback = sinon.stub() + return this.CollaboratorHandler.getAllInvitedMembers( + this.project_id, + this.callback + ) + }) + + it('should not produce an error', function() { + this.callback.callCount.should.equal(1) + return expect(this.callback.firstCall.args[0]).to.equal(null) + }) + + it('should produce a list of members', function() { + this.callback.callCount.should.equal(1) + return expect(this.callback.firstCall.args[1]).to.deep.equal( + this.views.members + ) + }) + + it('should call getMembersWithPrivileges', function() { + this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.callCount.should.equal( + 1 + ) + return this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.firstCall.args[0].should.equal( + this.project_id + ) + }) + + it('should call ProjectEditorHandler.buildOwnerAndMembersViews', function() { + this.ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal( + 1 + ) + return this.ProjectEditorHandler.buildOwnerAndMembersViews.firstCall.args[0].should.equal( + this.members + ) + }) + + return describe('when getMembersWithPrivileges produces an error', function() { + beforeEach(function() { + this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.ProjectEditorHandler.buildOwnerAndMembersViews = sinon + .stub() + .returns( + (this.views = { + owner: this.owning_user, + ownerFeatures: this.owning_user.features, + members: [ + { + _id: this.readwrite_user._id, + email: this.readwrite_user.email + } + ] + }) + ) + this.callback = sinon.stub() + return this.CollaboratorHandler.getAllInvitedMembers( + this.project_id, + this.callback + ) + }) + + it('should produce an error', function() { + this.callback.callCount.should.equal(1) + expect(this.callback.firstCall.args[0]).to.not.equal(null) + return expect(this.callback.firstCall.args[0]).to.be.instanceof(Error) + }) + + it('should call getMembersWithPrivileges', function() { + this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.callCount.should.equal( + 1 + ) + return this.CollaboratorHandler.getInvitedMembersWithPrivilegeLevels.firstCall.args[0].should.equal( + this.project_id + ) + }) + + return it('should not call ProjectEditorHandler.buildOwnerAndMembersViews', function() { + return this.ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal( + 0 + ) + }) + }) + }) + + describe('userIsTokenMember', function() { + beforeEach(function() { + this.user_id = ObjectId() + this.project_id = ObjectId() + this.project = { _id: this.project_id } + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, null, this.project)) + }) + + it('should check the database', function(done) { + return this.CollaboratorHandler.userIsTokenMember( + this.user_id, + this.project_id, + (err, isTokenMember) => { + this.Project.findOne.callCount.should.equal(1) + return done() + } + ) + }) + + it('should return true when the project is found', function(done) { + return this.CollaboratorHandler.userIsTokenMember( + this.user_id, + this.project_id, + (err, isTokenMember) => { + expect(err).to.not.exist + expect(isTokenMember).to.equal(true) + return done() + } + ) + }) + + return it('should return false when the project is not found', function(done) { + this.project = null + this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) + return this.CollaboratorHandler.userIsTokenMember( + this.user_id, + this.project_id, + (err, isTokenMember) => { + expect(err).to.not.exist + expect(isTokenMember).to.equal(false) + return done() + } + ) + }) + }) + + return describe('transferProjects', function() { + beforeEach(function() { + this.from_user_id = 'from-user-id' + this.to_user_id = 'to-user-id' + this.projects = [ + { + _id: 'project-id-1' + }, + { + _id: 'project-id-2' + } + ] + this.Project.find = sinon.stub().yields(null, this.projects) + this.Project.update = sinon.stub().yields() + return (this.ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon + .stub() + .yields()) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.CollaboratorHandler.transferProjects( + this.from_user_id, + this.to_user_id, + this.callback + ) + }) + + it('should look up the affected projects', function() { + return this.Project.find + .calledWith({ + $or: [ + { owner_ref: this.from_user_id }, + { collaberator_refs: this.from_user_id }, + { readOnly_refs: this.from_user_id } + ] + }) + .should.equal(true) + }) + + it('should transfer owned projects', function() { + return this.Project.update + .calledWith( + { + owner_ref: this.from_user_id + }, + { + $set: { owner_ref: this.to_user_id } + }, + { + multi: true + } + ) + .should.equal(true) + }) + + it('should transfer collaborator projects', function() { + this.Project.update + .calledWith( + { + collaberator_refs: this.from_user_id + }, + { + $addToSet: { collaberator_refs: this.to_user_id } + }, + { + multi: true + } + ) + .should.equal(true) + return this.Project.update + .calledWith( + { + collaberator_refs: this.from_user_id + }, + { + $pull: { collaberator_refs: this.from_user_id } + }, + { + multi: true + } + ) + .should.equal(true) + }) + + it('should transfer read only collaborator projects', function() { + this.Project.update + .calledWith( + { + readOnly_refs: this.from_user_id + }, + { + $addToSet: { readOnly_refs: this.to_user_id } + }, + { + multi: true + } + ) + .should.equal(true) + return this.Project.update + .calledWith( + { + readOnly_refs: this.from_user_id + }, + { + $pull: { readOnly_refs: this.from_user_id } + }, + { + multi: true + } + ) + .should.equal(true) + }) + + it('should flush each project to the TPDS', function() { + return Array.from(this.projects).map(project => + this.ProjectEntityHandler.flushProjectToThirdPartyDataStore + .calledWith(project._id) + .should.equal(true) + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when flushing to TPDS fails', function() { + beforeEach(function() { + this.ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon + .stub() + .yields(new Error('oops')) + return this.CollaboratorHandler.transferProjects( + this.from_user_id, + this.to_user_id, + this.callback + ) + }) + + it('should log an error', function() { + return this.logger.err.called.should.equal(true) + }) + + return it('should not return an error since it happens in the background', function() { + this.callback.called.should.equal(true) + return this.callback.calledWith(new Error('oops')).should.equal(false) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js new file mode 100644 index 0000000000..5e0816b895 --- /dev/null +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.js @@ -0,0 +1,1389 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Collaborators/CollaboratorsInviteController.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const { ObjectId } = require('mongojs') + +describe('CollaboratorsInviteController', function() { + beforeEach(function() { + this.user = { _id: 'id' } + this.AnalyticsManger = { recordEvent: sinon.stub() } + this.sendingUser = null + this.AuthenticationController = { + getSessionUser: req => { + this.sendingUser = req.session.user + return this.sendingUser + } + } + + this.RateLimiter = { addCount: sinon.stub } + + this.LimitationsManager = {} + this.UserGetter = { + getUserByAnyEmail: sinon.stub(), + getUser: sinon.stub() + } + + this.CollaboratorsInviteController = SandboxedModule.require(modulePath, { + requires: { + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../Subscription/LimitationsManager': this.LimitationsManager, + '../User/UserGetter': this.UserGetter, + './CollaboratorsHandler': (this.CollaboratorsHandler = {}), + './CollaboratorsInviteHandler': (this.CollaboratorsInviteHandler = {}), + 'logger-sharelatex': (this.logger = { + err: sinon.stub(), + error: sinon.stub(), + log: sinon.stub() + }), + '../Editor/EditorRealTimeController': (this.EditorRealTimeController = { + emitToRoom: sinon.stub() + }), + '../Notifications/NotificationsBuilder': (this.NotificationsBuilder = {}), + '../Analytics/AnalyticsManager': this.AnalyticsManger, + '../Authentication/AuthenticationController': this + .AuthenticationController, + 'settings-sharelatex': (this.settings = {}), + '../../infrastructure/RateLimiter': this.RateLimiter + } + }) + this.res = new MockResponse() + this.req = new MockRequest() + + this.project_id = 'project-id-123' + return (this.callback = sinon.stub()) + }) + + describe('getAllInvites', function() { + beforeEach(function() { + this.fakeInvites = [ + { _id: ObjectId(), one: 1 }, + { _id: ObjectId(), two: 2 } + ] + this.req.params = { Project_id: this.project_id } + this.res.json = sinon.stub() + return (this.next = sinon.stub()) + }) + + describe('when all goes well', function() { + beforeEach(function() { + this.CollaboratorsInviteHandler.getAllInvites = sinon + .stub() + .callsArgWith(1, null, this.fakeInvites) + return this.CollaboratorsInviteController.getAllInvites( + this.req, + this.res, + this.next + ) + }) + + it('should not produce an error', function() { + return this.next.callCount.should.equal(0) + }) + + it('should produce a list of invite objects', function() { + this.res.json.callCount.should.equal(1) + return this.res.json + .calledWith({ invites: this.fakeInvites }) + .should.equal(true) + }) + + return it('should have called CollaboratorsInviteHandler.getAllInvites', function() { + this.CollaboratorsInviteHandler.getAllInvites.callCount.should.equal(1) + return this.CollaboratorsInviteHandler.getAllInvites + .calledWith(this.project_id) + .should.equal(true) + }) + }) + + return describe('when CollaboratorsInviteHandler.getAllInvites produces an error', function() { + beforeEach(function() { + this.CollaboratorsInviteHandler.getAllInvites = sinon + .stub() + .callsArgWith(1, new Error('woops')) + return this.CollaboratorsInviteController.getAllInvites( + this.req, + this.res, + this.next + ) + }) + + return it('should produce an error', function() { + this.next.callCount.should.equal(1) + return this.next.firstCall.args[0].should.be.instanceof(Error) + }) + }) + }) + + describe('inviteToProject', function() { + beforeEach(function() { + this.targetEmail = 'user@example.com' + this.req.params = { Project_id: this.project_id } + this.current_user = { _id: (this.current_user_id = 'current-user-id') } + this.req.session = { user: this.current_user } + this.req.body = { + email: this.targetEmail, + privileges: (this.privileges = 'readAndWrite') + } + this.res.json = sinon.stub() + this.res.sendStatus = sinon.stub() + this.invite = { + _id: ObjectId(), + token: 'htnseuthaouse', + sendingUserId: this.current_user_id, + projectId: this.targetEmail, + targetEmail: 'user@example.com', + createdAt: new Date() + } + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, null, true) + this.CollaboratorsInviteHandler.inviteToProject = sinon + .stub() + .callsArgWith(4, null, this.invite) + this.err = new Error('woops') + this.callback = sinon.stub() + return (this.next = sinon.stub()) + }) + + describe('when all goes well', function() { + beforeEach(function() { + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, null, true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, null, true) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should produce json response', function() { + this.res.json.callCount.should.equal(1) + return { invite: this.invite }.should.deep.equal( + this.res.json.firstCall.args[0] + ) + }) + + it('should have called canAddXCollaborators', function() { + this.LimitationsManager.canAddXCollaborators.callCount.should.equal(1) + return this.LimitationsManager.canAddXCollaborators + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should have called _checkShouldInviteEmail', function() { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 1 + ) + return this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.targetEmail) + .should.equal(true) + }) + + it('should have called inviteToProject', function() { + this.CollaboratorsInviteHandler.inviteToProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsInviteHandler.inviteToProject + .calledWith( + this.project_id, + this.current_user, + this.targetEmail, + this.privileges + ) + .should.equal(true) + }) + + return it('should have called emitToRoom', function() { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:membership:changed') + .should.equal(true) + }) + }) + + describe('when the user is not allowed to add more collaborators', function() { + beforeEach(function() { + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, null, true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, null, false) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should produce json response without an invite', function() { + this.res.json.callCount.should.equal(1) + return { invite: null }.should.deep.equal( + this.res.json.firstCall.args[0] + ) + }) + + it('should not have called _checkShouldInviteEmail', function() { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 0 + ) + return this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.sendingUser, this.targetEmail) + .should.equal(false) + }) + + return it('should not have called inviteToProject', function() { + return this.CollaboratorsInviteHandler.inviteToProject.callCount.should.equal( + 0 + ) + }) + }) + + describe('when canAddXCollaborators produces an error', function() { + beforeEach(function() { + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, null, true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, this.err) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return this.next.calledWith(this.err).should.equal(true) + }) + + it('should not have called _checkShouldInviteEmail', function() { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 0 + ) + return this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.sendingUser, this.targetEmail) + .should.equal(false) + }) + + return it('should not have called inviteToProject', function() { + return this.CollaboratorsInviteHandler.inviteToProject.callCount.should.equal( + 0 + ) + }) + }) + + describe('when inviteToProject produces an error', function() { + beforeEach(function() { + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, null, true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.err = new Error('woops') + this.CollaboratorsInviteHandler.inviteToProject = sinon + .stub() + .callsArgWith(4, this.err) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return this.next.calledWith(this.err).should.equal(true) + }) + + it('should have called canAddXCollaborators', function() { + this.LimitationsManager.canAddXCollaborators.callCount.should.equal(1) + return this.LimitationsManager.canAddXCollaborators + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should have called _checkShouldInviteEmail', function() { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 1 + ) + return this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.targetEmail) + .should.equal(true) + }) + + return it('should have called inviteToProject', function() { + this.CollaboratorsInviteHandler.inviteToProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsInviteHandler.inviteToProject + .calledWith( + this.project_id, + this.current_user, + this.targetEmail, + this.privileges + ) + .should.equal(true) + }) + }) + + describe('when _checkShouldInviteEmail disallows the invite', function() { + beforeEach(function() { + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, null, false) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, null, true) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should produce json response with no invite, and an error property', function() { + this.res.json.callCount.should.equal(1) + return { + invite: null, + error: 'cannot_invite_non_user' + }.should.deep.equal(this.res.json.firstCall.args[0]) + }) + + it('should have called _checkShouldInviteEmail', function() { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 1 + ) + return this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.targetEmail) + .should.equal(true) + }) + + return it('should not have called inviteToProject', function() { + return this.CollaboratorsInviteHandler.inviteToProject.callCount.should.equal( + 0 + ) + }) + }) + + describe('when _checkShouldInviteEmail produces an error', function() { + beforeEach(function() { + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, null, true) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return this.next.calledWith(this.err).should.equal(true) + }) + + it('should have called _checkShouldInviteEmail', function() { + this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 1 + ) + return this.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(this.targetEmail) + .should.equal(true) + }) + + return it('should not have called inviteToProject', function() { + return this.CollaboratorsInviteHandler.inviteToProject.callCount.should.equal( + 0 + ) + }) + }) + + describe('when the user invites themselves to the project', function() { + beforeEach(function() { + this.req.session.user = { _id: 'abc', email: 'me@example.com' } + this.req.body.email = 'me@example.com' + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, null, true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, null, true) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should reject action, return json response with error code', function() { + this.res.json.callCount.should.equal(1) + return { invite: null, error: 'cannot_invite_self' }.should.deep.equal( + this.res.json.firstCall.args[0] + ) + }) + + it('should not have called canAddXCollaborators', function() { + return this.LimitationsManager.canAddXCollaborators.callCount.should.equal( + 0 + ) + }) + + it('should not have called _checkShouldInviteEmail', function() { + return this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + 0 + ) + }) + + it('should not have called inviteToProject', function() { + return this.CollaboratorsInviteHandler.inviteToProject.callCount.should.equal( + 0 + ) + }) + + return it('should not have called emitToRoom', function() { + return this.EditorRealTimeController.emitToRoom.callCount.should.equal( + 0 + ) + }) + }) + + return describe('when _checkRateLimit returns false', function() { + beforeEach(function() { + this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .callsArgWith(1, null, true) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, false) + this.LimitationsManager.canAddXCollaborators = sinon + .stub() + .callsArgWith(2, null, true) + return this.CollaboratorsInviteController.inviteToProject( + this.req, + this.res, + this.next + ) + }) + + it('should send a 429 response', function() { + return this.res.sendStatus.calledWith(429).should.equal(true) + }) + + it('should not call inviteToProject', function() { + return this.CollaboratorsInviteHandler.inviteToProject.called.should.equal( + false + ) + }) + + return it('should not call emitToRoom', function() { + return this.EditorRealTimeController.emitToRoom.called.should.equal( + false + ) + }) + }) + }) + + describe('viewInvite', function() { + beforeEach(function() { + this.token = 'some-opaque-token' + this.req.params = { + Project_id: this.project_id, + token: this.token + } + this.req.session = { + user: { _id: (this.current_user_id = 'current-user-id') } + } + this.res.render = sinon.stub() + this.res.redirect = sinon.stub() + this.res.sendStatus = sinon.stub() + this.invite = { + _id: ObjectId(), + token: this.token, + sendingUserId: ObjectId(), + projectId: this.project_id, + targetEmail: 'user@example.com', + createdAt: new Date() + } + this.fakeProject = { + _id: this.project_id, + name: 'some project', + owner_ref: this.invite.sendingUserId, + collaberator_refs: [], + readOnly_refs: [] + } + this.owner = { + _id: this.fakeProject.owner_ref, + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com' + } + + this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + .stub() + .callsArgWith(2, null, false, null) + this.CollaboratorsInviteHandler.getInviteByToken = sinon + .stub() + .callsArgWith(2, null, this.invite) + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.fakeProject) + this.UserGetter.getUser.callsArgWith(2, null, this.owner) + + this.callback = sinon.stub() + return (this.next = sinon.stub()) + }) + + describe('when the token is valid', function() { + beforeEach(function() { + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should render the view template', function() { + this.res.render.callCount.should.equal(1) + return this.res.render + .calledWith('project/invite/show') + .should.equal(true) + }) + + it('should not call next', function() { + return this.next.callCount.should.equal(0) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should call getInviteByToken', function() { + this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 1 + ) + return this.CollaboratorsInviteHandler.getInviteByToken + .calledWith(this.fakeProject._id, this.invite.token) + .should.equal(true) + }) + + it('should call User.getUser', function() { + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith({ _id: this.fakeProject.owner_ref }) + .should.equal(true) + }) + + return it('should call ProjectGetter.getProject', function() { + this.ProjectGetter.getProject.callCount.should.equal(1) + return this.ProjectGetter.getProject + .calledWith(this.project_id) + .should.equal(true) + }) + }) + + describe('when user is already a member of the project', function() { + beforeEach(function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + .stub() + .callsArgWith(2, null, true, null) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should redirect to the project page', function() { + this.res.redirect.callCount.should.equal(1) + return this.res.redirect + .calledWith(`/project/${this.project_id}`) + .should.equal(true) + }) + + it('should not call next with an error', function() { + return this.next.callCount.should.equal(0) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should not call getInviteByToken', function() { + return this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 0 + ) + }) + + it('should not call User.getUser', function() { + return this.UserGetter.getUser.callCount.should.equal(0) + }) + + return it('should not call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(0) + }) + }) + + describe('when isUserInvitedMemberOfProject produces an error', function() { + beforeEach(function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + .stub() + .callsArgWith(2, new Error('woops')) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should not call getInviteByToken', function() { + return this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 0 + ) + }) + + it('should not call User.getUser', function() { + return this.UserGetter.getUser.callCount.should.equal(0) + }) + + return it('should not call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(0) + }) + }) + + describe('when the getInviteByToken produces an error', function() { + beforeEach(function() { + this.err = new Error('woops') + this.CollaboratorsInviteHandler.getInviteByToken.callsArgWith( + 2, + this.err + ) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should call next with the error', function() { + this.next.callCount.should.equal(1) + return this.next.calledWith(this.err).should.equal(true) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should call getInviteByToken', function() { + this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should not call User.getUser', function() { + return this.UserGetter.getUser.callCount.should.equal(0) + }) + + return it('should not call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(0) + }) + }) + + describe('when the getInviteByToken does not produce an invite', function() { + beforeEach(function() { + this.CollaboratorsInviteHandler.getInviteByToken.callsArgWith( + 2, + null, + null + ) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should render the not-valid view template', function() { + this.res.render.callCount.should.equal(1) + return this.res.render + .calledWith('project/invite/not-valid') + .should.equal(true) + }) + + it('should not call next', function() { + return this.next.callCount.should.equal(0) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should call getInviteByToken', function() { + this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should not call User.getUser', function() { + return this.UserGetter.getUser.callCount.should.equal(0) + }) + + return it('should not call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(0) + }) + }) + + describe('when User.getUser produces an error', function() { + beforeEach(function() { + this.UserGetter.getUser.callsArgWith(2, new Error('woops')) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should produce an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should call getInviteByToken', function() { + return this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 1 + ) + }) + + it('should call User.getUser', function() { + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith({ _id: this.fakeProject.owner_ref }) + .should.equal(true) + }) + + return it('should not call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(0) + }) + }) + + describe('when User.getUser does not find a user', function() { + beforeEach(function() { + this.UserGetter.getUser.callsArgWith(2, null, null) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should render the not-valid view template', function() { + this.res.render.callCount.should.equal(1) + return this.res.render + .calledWith('project/invite/not-valid') + .should.equal(true) + }) + + it('should not call next', function() { + return this.next.callCount.should.equal(0) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should call getInviteByToken', function() { + return this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 1 + ) + }) + + it('should call User.getUser', function() { + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith({ _id: this.fakeProject.owner_ref }) + .should.equal(true) + }) + + return it('should not call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(0) + }) + }) + + describe('when getProject produces an error', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should produce an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should call getInviteByToken', function() { + return this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 1 + ) + }) + + it('should call User.getUser', function() { + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith({ _id: this.fakeProject.owner_ref }) + .should.equal(true) + }) + + return it('should call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(1) + }) + }) + + return describe('when Project.getUser does not find a user', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, null, null) + return this.CollaboratorsInviteController.viewInvite( + this.req, + this.res, + this.next + ) + }) + + it('should render the not-valid view template', function() { + this.res.render.callCount.should.equal(1) + return this.res.render + .calledWith('project/invite/not-valid') + .should.equal(true) + }) + + it('should not call next', function() { + return this.next.callCount.should.equal(0) + }) + + it('should call CollaboratorsHandler.isUserInvitedMemberOfProject', function() { + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount.should.equal( + 1 + ) + return this.CollaboratorsHandler.isUserInvitedMemberOfProject + .calledWith(this.current_user_id, this.project_id) + .should.equal(true) + }) + + it('should call getInviteByToken', function() { + return this.CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal( + 1 + ) + }) + + it('should call getUser', function() { + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith({ _id: this.fakeProject.owner_ref }) + .should.equal(true) + }) + + return it('should call ProjectGetter.getProject', function() { + return this.ProjectGetter.getProject.callCount.should.equal(1) + }) + }) + }) + + describe('resendInvite', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + invite_id: (this.invite_id = 'thuseoautoh') + } + this.req.session = { + user: { _id: (this.current_user_id = 'current-user-id') } + } + this.res.render = sinon.stub() + this.res.sendStatus = sinon.stub() + this.CollaboratorsInviteHandler.resendInvite = sinon + .stub() + .callsArgWith(3, null) + this.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .yields(null, true) + this.callback = sinon.stub() + return (this.next = sinon.stub()) + }) + + describe('when resendInvite does not produce an error', function() { + beforeEach(function() { + return this.CollaboratorsInviteController.resendInvite( + this.req, + this.res, + this.next + ) + }) + + it('should produce a 201 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(201).should.equal(true) + }) + + it('should have called resendInvite', function() { + return this.CollaboratorsInviteHandler.resendInvite.callCount.should.equal( + 1 + ) + }) + + return it('should check the rate limit', function() { + return this.CollaboratorsInviteController._checkRateLimit.callCount.should.equal( + 1 + ) + }) + }) + + return describe('when resendInvite produces an error', function() { + beforeEach(function() { + this.err = new Error('woops') + this.CollaboratorsInviteHandler.resendInvite = sinon + .stub() + .callsArgWith(3, this.err) + return this.CollaboratorsInviteController.resendInvite( + this.req, + this.res, + this.next + ) + }) + + it('should not produce a 201 response', function() { + return this.res.sendStatus.callCount.should.equal(0) + }) + + it('should call next with the error', function() { + this.next.callCount.should.equal(1) + return this.next.calledWith(this.err).should.equal(true) + }) + + return it('should have called resendInvite', function() { + return this.CollaboratorsInviteHandler.resendInvite.callCount.should.equal( + 1 + ) + }) + }) + }) + + describe('revokeInvite', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + invite_id: (this.invite_id = 'thuseoautoh') + } + this.current_user = { _id: (this.current_user_id = 'current-user-id') } + this.req.session = { user: this.current_user } + this.res.render = sinon.stub() + this.res.sendStatus = sinon.stub() + this.CollaboratorsInviteHandler.revokeInvite = sinon + .stub() + .callsArgWith(2, null) + this.callback = sinon.stub() + return (this.next = sinon.stub()) + }) + + describe('when revokeInvite does not produce an error', function() { + beforeEach(function() { + return this.CollaboratorsInviteController.revokeInvite( + this.req, + this.res, + this.next + ) + }) + + it('should produce a 201 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(201).should.equal(true) + }) + + it('should have called revokeInvite', function() { + return this.CollaboratorsInviteHandler.revokeInvite.callCount.should.equal( + 1 + ) + }) + + return it('should have called emitToRoom', function() { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:membership:changed') + .should.equal(true) + }) + }) + + return describe('when revokeInvite produces an error', function() { + beforeEach(function() { + this.err = new Error('woops') + this.CollaboratorsInviteHandler.revokeInvite = sinon + .stub() + .callsArgWith(2, this.err) + return this.CollaboratorsInviteController.revokeInvite( + this.req, + this.res, + this.next + ) + }) + + it('should not produce a 201 response', function() { + return this.res.sendStatus.callCount.should.equal(0) + }) + + it('should call next with the error', function() { + this.next.callCount.should.equal(1) + return this.next.calledWith(this.err).should.equal(true) + }) + + return it('should have called revokeInvite', function() { + return this.CollaboratorsInviteHandler.revokeInvite.callCount.should.equal( + 1 + ) + }) + }) + }) + + describe('acceptInvite', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + token: (this.token = 'mock-token') + } + this.req.session = { + user: { _id: (this.current_user_id = 'current-user-id') } + } + this.res.render = sinon.stub() + this.res.redirect = sinon.stub() + this.CollaboratorsInviteHandler.acceptInvite = sinon + .stub() + .callsArgWith(3, null) + this.callback = sinon.stub() + return (this.next = sinon.stub()) + }) + + describe('when acceptInvite does not produce an error', function() { + beforeEach(function() { + return this.CollaboratorsInviteController.acceptInvite( + this.req, + this.res, + this.next + ) + }) + + it('should redirect to project page', function() { + this.res.redirect.callCount.should.equal(1) + return this.res.redirect + .calledWith(`/project/${this.project_id}`) + .should.equal(true) + }) + + it('should have called acceptInvite', function() { + return this.CollaboratorsInviteHandler.acceptInvite + .calledWith(this.project_id, this.token) + .should.equal(true) + }) + + return it('should have called emitToRoom', function() { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:membership:changed') + .should.equal(true) + }) + }) + + return describe('when revokeInvite produces an error', function() { + beforeEach(function() { + this.err = new Error('woops') + this.CollaboratorsInviteHandler.acceptInvite = sinon + .stub() + .callsArgWith(3, this.err) + return this.CollaboratorsInviteController.acceptInvite( + this.req, + this.res, + this.next + ) + }) + + it('should not redirect to project page', function() { + return this.res.redirect.callCount.should.equal(0) + }) + + it('should call next with the error', function() { + this.next.callCount.should.equal(1) + return this.next.calledWith(this.err).should.equal(true) + }) + + return it('should have called acceptInvite', function() { + return this.CollaboratorsInviteHandler.acceptInvite.callCount.should.equal( + 1 + ) + }) + }) + }) + + describe('_checkShouldInviteEmail', function() { + beforeEach(function() { + return (this.email = 'user@example.com') + }) + + return describe('when we should be restricting to existing accounts', function() { + beforeEach(function() { + this.settings.restrictInvitesToExistingAccounts = true + return (this.call = callback => { + return this.CollaboratorsInviteController._checkShouldInviteEmail( + this.email, + callback + ) + }) + }) + + describe('when user account is present', function() { + beforeEach(function() { + this.user = { _id: ObjectId().toString() } + return (this.UserGetter.getUserByAnyEmail = sinon + .stub() + .callsArgWith(2, null, this.user)) + }) + + return it('should callback with `true`', function(done) { + return this.call((err, shouldAllow) => { + expect(err).to.equal(null) + expect(shouldAllow).to.equal(true) + return done() + }) + }) + }) + + describe('when user account is absent', function() { + beforeEach(function() { + this.user = null + return (this.UserGetter.getUserByAnyEmail = sinon + .stub() + .callsArgWith(2, null, this.user)) + }) + + it('should callback with `false`', function(done) { + return this.call((err, shouldAllow) => { + expect(err).to.equal(null) + expect(shouldAllow).to.equal(false) + return done() + }) + }) + + return it('should have called getUser', function(done) { + return this.call((err, shouldAllow) => { + this.UserGetter.getUserByAnyEmail.callCount.should.equal(1) + this.UserGetter.getUserByAnyEmail + .calledWith(this.email, { _id: 1 }) + .should.equal(true) + return done() + }) + }) + }) + + return describe('when getUser produces an error', function() { + beforeEach(function() { + this.user = null + return (this.UserGetter.getUserByAnyEmail = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should callback with an error', function(done) { + return this.call((err, shouldAllow) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(shouldAllow).to.equal(undefined) + return done() + }) + }) + }) + }) + }) + + return describe('_checkRateLimit', function() { + beforeEach(function() { + this.settings.restrictInvitesToExistingAccounts = false + this.sendingUserId = '32312313' + this.LimitationsManager.allowedNumberOfCollaboratorsForUser = sinon.stub() + return this.LimitationsManager.allowedNumberOfCollaboratorsForUser + .withArgs(this.sendingUserId) + .yields(null, 17) + }) + + it('should callback with `true` when rate limit under', function(done) { + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + return this.CollaboratorsInviteController._checkRateLimit( + this.sendingUserId, + (err, result) => { + this.RateLimiter.addCount.called.should.equal(true) + result.should.equal(true) + return done() + } + ) + }) + + it('should callback with `false` when rate limit hit', function(done) { + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) + return this.CollaboratorsInviteController._checkRateLimit( + this.sendingUserId, + (err, result) => { + this.RateLimiter.addCount.called.should.equal(true) + result.should.equal(false) + return done() + } + ) + }) + + it('should call rate limiter with 10x the collaborators', function(done) { + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + return this.CollaboratorsInviteController._checkRateLimit( + this.sendingUserId, + (err, result) => { + this.RateLimiter.addCount.args[0][0].throttle.should.equal(170) + return done() + } + ) + }) + + it('should call rate limiter with 200 when collaborators is -1', function(done) { + this.LimitationsManager.allowedNumberOfCollaboratorsForUser + .withArgs(this.sendingUserId) + .yields(null, -1) + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + return this.CollaboratorsInviteController._checkRateLimit( + this.sendingUserId, + (err, result) => { + this.RateLimiter.addCount.args[0][0].throttle.should.equal(200) + return done() + } + ) + }) + + return it('should call rate limiter with 10 when user has no collaborators set', function(done) { + this.LimitationsManager.allowedNumberOfCollaboratorsForUser + .withArgs(this.sendingUserId) + .yields(null) + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + return this.CollaboratorsInviteController._checkRateLimit( + this.sendingUserId, + (err, result) => { + this.RateLimiter.addCount.args[0][0].throttle.should.equal(10) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js new file mode 100644 index 0000000000..f4b5f71390 --- /dev/null +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js @@ -0,0 +1,1145 @@ +/* eslint-disable + chai-friendly/no-unused-expressions, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const { ObjectId } = require('mongojs') +const Crypto = require('crypto') + +describe('CollaboratorsInviteHandler', function() { + beforeEach(function() { + let ProjectInvite + this.ProjectInvite = ProjectInvite = (function() { + ProjectInvite = class ProjectInvite { + static initClass() { + this.prototype.save = sinon.stub() + this.findOne = sinon.stub() + this.find = sinon.stub() + this.remove = sinon.stub() + this.count = sinon.stub() + } + constructor(options) { + if (options == null) { + options = {} + } + this._id = ObjectId() + for (let k in options) { + const v = options[k] + this[k] = v + } + this + } + } + ProjectInvite.initClass() + return ProjectInvite + })() + this.Crypto = Crypto + this.CollaboratorsInviteHandler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = {}), + '../../models/ProjectInvite': { ProjectInvite: this.ProjectInvite }, + 'logger-sharelatex': (this.logger = { + err: sinon.stub(), + error: sinon.stub(), + log: sinon.stub() + }), + './CollaboratorsEmailHandler': (this.CollaboratorsEmailHandler = {}), + './CollaboratorsHandler': (this.CollaboratorsHandler = { + addUserIdToProject: sinon.stub() + }), + '../User/UserGetter': (this.UserGetter = { getUser: sinon.stub() }), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../Notifications/NotificationsBuilder': (this.NotificationsBuilder = {}), + crypto: this.Crypto + } + }) + + this.projectId = ObjectId() + this.sendingUserId = ObjectId() + this.sendingUser = { + _id: this.sendingUserId, + name: 'Bob' + } + this.email = 'user@example.com' + this.userId = ObjectId() + this.user = { + _id: this.userId, + email: 'someone@example.com' + } + this.inviteId = ObjectId() + this.token = 'hnhteaosuhtaeosuahs' + this.privileges = 'readAndWrite' + return (this.fakeInvite = { + _id: this.inviteId, + email: this.email, + token: this.token, + sendingUserId: this.sendingUserId, + projectId: this.projectId, + privileges: this.privileges, + createdAt: new Date() + }) + }) + + describe('getInviteCount', function() { + beforeEach(function() { + this.ProjectInvite.count.callsArgWith(1, null, 2) + return (this.call = callback => { + return this.CollaboratorsInviteHandler.getInviteCount( + this.projectId, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, invites) => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should produce the count of documents', function(done) { + return this.call((err, count) => { + expect(count).to.equal(2) + return done() + }) + }) + + return describe('when model.count produces an error', function() { + beforeEach(function() { + return this.ProjectInvite.count.callsArgWith(1, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call((err, count) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('getAllInvites', function() { + beforeEach(function() { + this.fakeInvites = [ + { _id: ObjectId(), one: 1 }, + { _id: ObjectId(), two: 2 } + ] + this.ProjectInvite.find.callsArgWith(1, null, this.fakeInvites) + return (this.call = callback => { + return this.CollaboratorsInviteHandler.getAllInvites( + this.projectId, + callback + ) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() {}) + + it('should not produce an error', function(done) { + return this.call((err, invites) => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should produce a list of invite objects', function(done) { + return this.call((err, invites) => { + expect(invites).to.not.be.oneOf([null, undefined]) + expect(invites).to.deep.equal(this.fakeInvites) + return done() + }) + }) + + return it('should have called ProjectInvite.find', function(done) { + return this.call((err, invites) => { + this.ProjectInvite.find.callCount.should.equal(1) + this.ProjectInvite.find + .calledWith({ projectId: this.projectId }) + .should.equal(true) + return done() + }) + }) + }) + + return describe('when ProjectInvite.find produces an error', function() { + beforeEach(function() { + return this.ProjectInvite.find.callsArgWith(1, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call((err, invites) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('inviteToProject', function() { + beforeEach(function() { + this.ProjectInvite.prototype.save = sinon.spy(function(cb) { + return cb(null, this) + }) + this.randomBytesSpy = sinon.spy(this.Crypto, 'randomBytes') + this.CollaboratorsInviteHandler._sendMessages = sinon + .stub() + .callsArgWith(3, null) + return (this.call = callback => { + return this.CollaboratorsInviteHandler.inviteToProject( + this.projectId, + this.sendingUser, + this.email, + this.privileges, + callback + ) + }) + }) + + afterEach(function() { + return this.randomBytesSpy.restore() + }) + + describe('when all goes well', function() { + beforeEach(function() {}) + + it('should not produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should produce the invite object', function(done) { + return this.call((err, invite) => { + expect(invite).to.not.equal(null) + expect(invite).to.not.equal(undefined) + expect(invite).to.be.instanceof(Object) + expect(invite).to.have.all.keys([ + '_id', + 'email', + 'token', + 'sendingUserId', + 'projectId', + 'privileges' + ]) + return done() + }) + }) + + it('should have generated a random token', function(done) { + return this.call((err, invite) => { + this.randomBytesSpy.callCount.should.equal(1) + return done() + }) + }) + + it('should have called ProjectInvite.save', function(done) { + return this.call((err, invite) => { + this.ProjectInvite.prototype.save.callCount.should.equal(1) + return done() + }) + }) + + return it('should have called _sendMessages', function(done) { + return this.call((err, invite) => { + this.CollaboratorsInviteHandler._sendMessages.callCount.should.equal( + 1 + ) + this.CollaboratorsInviteHandler._sendMessages + .calledWith(this.projectId, this.sendingUser) + .should.equal(true) + return done() + }) + }) + }) + + return describe('when saving model produces an error', function() { + beforeEach(function() { + return (this.ProjectInvite.prototype.save = sinon.spy(function(cb) { + return cb(new Error('woops'), this) + })) + }) + + return it('should produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('_sendMessages', function() { + beforeEach(function() { + this.CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon + .stub() + .callsArgWith(4, null) + this.CollaboratorsInviteHandler._trySendInviteNotification = sinon + .stub() + .callsArgWith(3, null) + return (this.call = callback => { + return this.CollaboratorsInviteHandler._sendMessages( + this.projectId, + this.sendingUser, + this.fakeInvite, + callback + ) + }) + }) + + describe('when all goes well', function() { + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', function(done) { + return this.call(err => { + this.CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal( + 1 + ) + this.CollaboratorsEmailHandler.notifyUserOfProjectInvite + .calledWith(this.projectId, this.fakeInvite.email, this.fakeInvite) + .should.equal(true) + return done() + }) + }) + + return it('should call _trySendInviteNotification', function(done) { + return this.call(err => { + this.CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal( + 1 + ) + this.CollaboratorsInviteHandler._trySendInviteNotification + .calledWith(this.projectId, this.sendingUser, this.fakeInvite) + .should.equal(true) + return done() + }) + }) + }) + + describe('when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', function() { + beforeEach(function() { + return (this.CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon + .stub() + .callsArgWith(4, new Error('woops'))) + }) + + it('should produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not call _trySendInviteNotification', function(done) { + return this.call(err => { + this.CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal( + 0 + ) + return done() + }) + }) + }) + + return describe('when _trySendInviteNotification produces an error', function() { + beforeEach(function() { + return (this.CollaboratorsInviteHandler._trySendInviteNotification = sinon + .stub() + .callsArgWith(3, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('revokeInvite', function() { + beforeEach(function() { + this.ProjectInvite.remove.callsArgWith(1, null) + this.CollaboratorsInviteHandler._tryCancelInviteNotification = sinon + .stub() + .callsArgWith(1, null) + return (this.call = callback => { + return this.CollaboratorsInviteHandler.revokeInvite( + this.projectId, + this.inviteId, + callback + ) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() {}) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should call ProjectInvite.remove', function(done) { + return this.call(err => { + this.ProjectInvite.remove.callCount.should.equal(1) + this.ProjectInvite.remove + .calledWith({ projectId: this.projectId, _id: this.inviteId }) + .should.equal(true) + return done() + }) + }) + + return it('should call _tryCancelInviteNotification', function(done) { + return this.call(err => { + this.CollaboratorsInviteHandler._tryCancelInviteNotification.callCount.should.equal( + 1 + ) + this.CollaboratorsInviteHandler._tryCancelInviteNotification + .calledWith(this.inviteId) + .should.equal(true) + return done() + }) + }) + }) + + return describe('when remove produces an error', function() { + beforeEach(function() { + return this.ProjectInvite.remove.callsArgWith(1, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('resendInvite', function() { + beforeEach(function() { + this.ProjectInvite.findOne.callsArgWith(1, null, this.fakeInvite) + this.CollaboratorsInviteHandler._sendMessages = sinon + .stub() + .callsArgWith(3, null) + return (this.call = callback => { + return this.CollaboratorsInviteHandler.resendInvite( + this.projectId, + this.sendingUser, + this.inviteId, + callback + ) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() {}) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should call ProjectInvite.findOne', function(done) { + return this.call((err, invite) => { + this.ProjectInvite.findOne.callCount.should.equal(1) + this.ProjectInvite.findOne + .calledWith({ _id: this.inviteId, projectId: this.projectId }) + .should.equal(true) + return done() + }) + }) + + return it('should have called _sendMessages', function(done) { + return this.call((err, invite) => { + this.CollaboratorsInviteHandler._sendMessages.callCount.should.equal( + 1 + ) + this.CollaboratorsInviteHandler._sendMessages + .calledWith(this.projectId, this.sendingUser, this.fakeInvite) + .should.equal(true) + return done() + }) + }) + }) + + describe('when findOne produces an error', function() { + beforeEach(function() { + return this.ProjectInvite.findOne.callsArgWith(1, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not have called _sendMessages', function(done) { + return this.call((err, invite) => { + this.CollaboratorsInviteHandler._sendMessages.callCount.should.equal( + 0 + ) + return done() + }) + }) + }) + + return describe('when findOne does not find an invite', function() { + beforeEach(function() { + return this.ProjectInvite.findOne.callsArgWith(1, null, null) + }) + + it('should not produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + return it('should not have called _sendMessages', function(done) { + return this.call((err, invite) => { + this.CollaboratorsInviteHandler._sendMessages.callCount.should.equal( + 0 + ) + return done() + }) + }) + }) + }) + + describe('getInviteByToken', function() { + beforeEach(function() { + this.ProjectInvite.findOne.callsArgWith(1, null, this.fakeInvite) + return (this.call = callback => { + return this.CollaboratorsInviteHandler.getInviteByToken( + this.projectId, + this.token, + callback + ) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() {}) + + it('should not produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should produce the invite object', function(done) { + return this.call((err, invite) => { + expect(invite).to.deep.equal(this.fakeInvite) + return done() + }) + }) + + return it('should call ProjectInvite.findOne', function(done) { + return this.call((err, invite) => { + this.ProjectInvite.findOne.callCount.should.equal(1) + this.ProjectInvite.findOne + .calledWith({ projectId: this.projectId, token: this.token }) + .should.equal(true) + return done() + }) + }) + }) + + describe('when findOne produces an error', function() { + beforeEach(function() { + return this.ProjectInvite.findOne.callsArgWith(1, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + + return describe('when findOne does not find an invite', function() { + beforeEach(function() { + return this.ProjectInvite.findOne.callsArgWith(1, null, null) + }) + + it('should not produce an error', function(done) { + return this.call((err, invite) => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + return it('should not produce an invite object', function(done) { + return this.call((err, invite) => { + expect(invite).to.not.be.instanceof(Error) + expect(invite).to.be.oneOf([null, undefined]) + return done() + }) + }) + }) + }) + + describe('acceptInvite', function() { + beforeEach(function() { + this.fakeProject = { + _id: this.projectId, + collaberator_refs: [], + readOnly_refs: [] + } + this.CollaboratorsHandler.addUserIdToProject.callsArgWith(4, null) + this._getInviteByToken = sinon.stub( + this.CollaboratorsInviteHandler, + 'getInviteByToken' + ) + this._getInviteByToken.callsArgWith(2, null, this.fakeInvite) + this.CollaboratorsInviteHandler._tryCancelInviteNotification = sinon + .stub() + .callsArgWith(1, null) + this.ProjectInvite.remove.callsArgWith(1, null) + return (this.call = callback => { + return this.CollaboratorsInviteHandler.acceptInvite( + this.projectId, + this.token, + this.user, + callback + ) + }) + }) + + afterEach(function() { + return this._getInviteByToken.restore() + }) + + describe('when all goes well', function() { + beforeEach(function() {}) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should have called getInviteByToken', function(done) { + return this.call(err => { + this._getInviteByToken.callCount.should.equal(1) + this._getInviteByToken + .calledWith(this.projectId, this.token) + .should.equal(true) + return done() + }) + }) + + it('should have called CollaboratorsHandler.addUserIdToProject', function(done) { + return this.call(err => { + this.CollaboratorsHandler.addUserIdToProject.callCount.should.equal(1) + this.CollaboratorsHandler.addUserIdToProject + .calledWith( + this.projectId, + this.sendingUserId, + this.userId, + this.fakeInvite.privileges + ) + .should.equal(true) + return done() + }) + }) + + return it('should have called ProjectInvite.remove', function(done) { + return this.call(err => { + this.ProjectInvite.remove.callCount.should.equal(1) + this.ProjectInvite.remove + .calledWith({ _id: this.inviteId }) + .should.equal(true) + return done() + }) + }) + }) + + describe('when the invite is for readOnly access', function() { + beforeEach(function() { + this.fakeInvite.privileges = 'readOnly' + return this._getInviteByToken.callsArgWith(2, null, this.fakeInvite) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + return it('should have called CollaboratorsHandler.addUserIdToProject', function(done) { + return this.call(err => { + this.CollaboratorsHandler.addUserIdToProject.callCount.should.equal(1) + this.CollaboratorsHandler.addUserIdToProject + .calledWith( + this.projectId, + this.sendingUserId, + this.userId, + this.fakeInvite.privileges + ) + .should.equal(true) + return done() + }) + }) + }) + + describe('when getInviteByToken does not find an invite', function() { + beforeEach(function() { + return this._getInviteByToken.callsArgWith(2, null, null) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + expect(err.name).to.equal('NotFoundError') + return done() + }) + }) + + it('should have called getInviteByToken', function(done) { + return this.call(err => { + this._getInviteByToken.callCount.should.equal(1) + this._getInviteByToken + .calledWith(this.projectId, this.token) + .should.equal(true) + return done() + }) + }) + + it('should not have called CollaboratorsHandler.addUserIdToProject', function(done) { + return this.call(err => { + this.CollaboratorsHandler.addUserIdToProject.callCount.should.equal(0) + return done() + }) + }) + + return it('should not have called ProjectInvite.remove', function(done) { + return this.call(err => { + this.ProjectInvite.remove.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when getInviteByToken produces an error', function() { + beforeEach(function() { + return this._getInviteByToken.callsArgWith(2, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + it('should have called getInviteByToken', function(done) { + return this.call(err => { + this._getInviteByToken.callCount.should.equal(1) + this._getInviteByToken + .calledWith(this.projectId, this.token) + .should.equal(true) + return done() + }) + }) + + it('should not have called CollaboratorsHandler.addUserIdToProject', function(done) { + return this.call(err => { + this.CollaboratorsHandler.addUserIdToProject.callCount.should.equal(0) + return done() + }) + }) + + return it('should not have called ProjectInvite.remove', function(done) { + return this.call(err => { + this.ProjectInvite.remove.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when addUserIdToProject produces an error', function() { + beforeEach(function() { + return this.CollaboratorsHandler.addUserIdToProject.callsArgWith( + 4, + new Error('woops') + ) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + it('should have called getInviteByToken', function(done) { + return this.call(err => { + this._getInviteByToken.callCount.should.equal(1) + this._getInviteByToken + .calledWith(this.projectId, this.token) + .should.equal(true) + return done() + }) + }) + + it('should have called CollaboratorsHandler.addUserIdToProject', function(done) { + return this.call(err => { + this.CollaboratorsHandler.addUserIdToProject.callCount.should.equal(1) + this.CollaboratorsHandler.addUserIdToProject + .calledWith( + this.projectId, + this.sendingUserId, + this.userId, + this.fakeInvite.privileges + ) + .should.equal(true) + return done() + }) + }) + + return it('should not have called ProjectInvite.remove', function(done) { + return this.call(err => { + this.ProjectInvite.remove.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when ProjectInvite.remove produces an error', function() { + beforeEach(function() { + return this.ProjectInvite.remove.callsArgWith(1, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + it('should have called getInviteByToken', function(done) { + return this.call(err => { + this._getInviteByToken.callCount.should.equal(1) + this._getInviteByToken + .calledWith(this.projectId, this.token) + .should.equal(true) + return done() + }) + }) + + it('should have called CollaboratorsHandler.addUserIdToProject', function(done) { + return this.call(err => { + this.CollaboratorsHandler.addUserIdToProject.callCount.should.equal(1) + this.CollaboratorsHandler.addUserIdToProject + .calledWith( + this.projectId, + this.sendingUserId, + this.userId, + this.fakeInvite.privileges + ) + .should.equal(true) + return done() + }) + }) + + return it('should have called ProjectInvite.remove', function(done) { + return this.call(err => { + this.ProjectInvite.remove.callCount.should.equal(1) + return done() + }) + }) + }) + }) + + describe('_tryCancelInviteNotification', function() { + beforeEach(function() { + this.inviteId = ObjectId() + this.currentUser = { _id: ObjectId() } + this.notification = { read: sinon.stub().callsArgWith(0, null) } + this.NotificationsBuilder.projectInvite = sinon + .stub() + .returns(this.notification) + return (this.call = callback => { + return this.CollaboratorsInviteHandler._tryCancelInviteNotification( + this.inviteId, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should call notification.read', function(done) { + return this.call(err => { + this.notification.read.callCount.should.equal(1) + return done() + }) + }) + + return describe('when notification.read produces an error', function() { + beforeEach(function() { + this.notification = { + read: sinon.stub().callsArgWith(0, new Error('woops')) + } + return (this.NotificationsBuilder.projectInvite = sinon + .stub() + .returns(this.notification)) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + return describe('_trySendInviteNotification', function() { + beforeEach(function() { + this.invite = { + _id: ObjectId(), + token: 'some_token', + sendingUserId: ObjectId(), + projectId: this.project_id, + targetEmail: 'user@example.com', + createdAt: new Date() + } + this.sendingUser = { + _id: ObjectId(), + first_name: 'jim' + } + this.existingUser = { _id: ObjectId() } + this.UserGetter.getUserByAnyEmail = sinon + .stub() + .callsArgWith(2, null, this.existingUser) + this.fakeProject = { + _id: this.project_id, + name: 'some project' + } + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.fakeProject) + this.notification = { create: sinon.stub().callsArgWith(0, null) } + this.NotificationsBuilder.projectInvite = sinon + .stub() + .returns(this.notification) + return (this.call = callback => { + return this.CollaboratorsInviteHandler._trySendInviteNotification( + this.project_id, + this.sendingUser, + this.invite, + callback + ) + }) + }) + + describe('when the user exists', function() { + beforeEach(function() {}) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should call getUser', function(done) { + return this.call(err => { + this.UserGetter.getUserByAnyEmail.callCount.should.equal(1) + this.UserGetter.getUserByAnyEmail + .calledWith(this.invite.email) + .should.equal(true) + return done() + }) + }) + + it('should call getProject', function(done) { + return this.call(err => { + this.ProjectGetter.getProject.callCount.should.equal(1) + this.ProjectGetter.getProject + .calledWith(this.project_id) + .should.equal(true) + return done() + }) + }) + + it('should call NotificationsBuilder.projectInvite.create', function(done) { + return this.call(err => { + this.NotificationsBuilder.projectInvite.callCount.should.equal(1) + this.notification.create.callCount.should.equal(1) + return done() + }) + }) + + describe('when getProject produces an error', function() { + beforeEach(function() { + return this.ProjectGetter.getProject.callsArgWith( + 2, + new Error('woops') + ) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not call NotificationsBuilder.projectInvite.create', function(done) { + return this.call(err => { + this.NotificationsBuilder.projectInvite.callCount.should.equal(0) + this.notification.create.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when projectInvite.create produces an error', function() { + beforeEach(function() { + return this.notification.create.callsArgWith(0, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('when the user does not exist', function() { + beforeEach(function() { + return (this.UserGetter.getUserByAnyEmail = sinon + .stub() + .callsArgWith(2, null, null)) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.be.oneOf([null, undefined]) + return done() + }) + }) + + it('should call getUser', function(done) { + return this.call(err => { + this.UserGetter.getUserByAnyEmail.callCount.should.equal(1) + this.UserGetter.getUserByAnyEmail + .calledWith(this.invite.email) + .should.equal(true) + return done() + }) + }) + + it('should not call getProject', function(done) { + return this.call(err => { + this.ProjectGetter.getProject.callCount.should.equal(0) + return done() + }) + }) + + return it('should not call NotificationsBuilder.projectInvite.create', function(done) { + return this.call(err => { + this.NotificationsBuilder.projectInvite.callCount.should.equal(0) + this.notification.create.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when the getUser produces an error', function() { + beforeEach(function() { + return (this.UserGetter.getUserByAnyEmail = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + it('should call getUser', function(done) { + return this.call(err => { + this.UserGetter.getUserByAnyEmail.callCount.should.equal(1) + this.UserGetter.getUserByAnyEmail + .calledWith(this.invite.email) + .should.equal(true) + return done() + }) + }) + + it('should not call getProject', function(done) { + return this.call(err => { + this.ProjectGetter.getProject.callCount.should.equal(0) + return done() + }) + }) + + return it('should not call NotificationsBuilder.projectInvite.create', function(done) { + return this.call(err => { + this.NotificationsBuilder.projectInvite.callCount.should.equal(0) + this.notification.create.callCount.should.equal(0) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Compile/ClsiCookieManagerTests.js b/services/web/test/unit/src/Compile/ClsiCookieManagerTests.js new file mode 100644 index 0000000000..ea17d4ef5c --- /dev/null +++ b/services/web/test/unit/src/Compile/ClsiCookieManagerTests.js @@ -0,0 +1,299 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const { assert } = chai +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Compile/ClsiCookieManager.js' +const SandboxedModule = require('sandboxed-module') +const realRequst = require('request') + +describe('ClsiCookieManager', function() { + beforeEach(function() { + const self = this + this.redisMulti = { + set: sinon.stub(), + get: sinon.stub(), + expire: sinon.stub(), + exec: sinon.stub() + } + this.redis = { + auth() {}, + get: sinon.stub(), + multi() { + return self.redisMulti + } + } + this.project_id = '123423431321' + this.request = { + get: sinon.stub(), + cookie: realRequst.cookie, + jar: realRequst.jar + } + this.settings = { + redis: { + web: 'redis.something' + }, + apis: { + clsi: { + url: 'http://clsi.example.com' + } + }, + clsiCookie: { + ttl: Math.random(), + key: 'coooookie' + } + } + this.requires = { + '../../infrastructure/RedisWrapper': (this.RedisWrapper = { + client: () => this.redis + }), + 'settings-sharelatex': this.settings, + request: this.request, + + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }) + } + return (this.ClsiCookieManager = SandboxedModule.require(modulePath, { + requires: this.requires + })()) + }) + + describe('getServerId', function() { + it('should call get for the key', function(done) { + this.redis.get.callsArgWith(1, null, 'clsi-7') + return this.ClsiCookieManager._getServerId( + this.project_id, + (err, serverId) => { + this.redis.get + .calledWith(`clsiserver:${this.project_id}`) + .should.equal(true) + serverId.should.equal('clsi-7') + return done() + } + ) + }) + + it('should _populateServerIdViaRequest if no key is found', function(done) { + this.ClsiCookieManager._populateServerIdViaRequest = sinon + .stub() + .callsArgWith(1) + this.redis.get.callsArgWith(1, null) + return this.ClsiCookieManager._getServerId( + this.project_id, + (err, serverId) => { + this.ClsiCookieManager._populateServerIdViaRequest + .calledWith(this.project_id) + .should.equal(true) + return done() + } + ) + }) + + return it('should _populateServerIdViaRequest if no key is blank', function(done) { + this.ClsiCookieManager._populateServerIdViaRequest = sinon + .stub() + .callsArgWith(1) + this.redis.get.callsArgWith(1, null, '') + return this.ClsiCookieManager._getServerId( + this.project_id, + (err, serverId) => { + this.ClsiCookieManager._populateServerIdViaRequest + .calledWith(this.project_id) + .should.equal(true) + return done() + } + ) + }) + }) + + describe('_populateServerIdViaRequest', function() { + beforeEach(function() { + this.response = 'some data' + this.request.get.callsArgWith(1, null, this.response) + return (this.ClsiCookieManager.setServerId = sinon + .stub() + .callsArgWith(2, null, 'clsi-9')) + }) + + it('should make a request to the clsi', function(done) { + return this.ClsiCookieManager._populateServerIdViaRequest( + this.project_id, + (err, serverId) => { + const args = this.ClsiCookieManager.setServerId.args[0] + args[0].should.equal(this.project_id) + args[1].should.deep.equal(this.response) + return done() + } + ) + }) + + return it('should return the server id', function(done) { + return this.ClsiCookieManager._populateServerIdViaRequest( + this.project_id, + (err, serverId) => { + serverId.should.equal('clsi-9') + return done() + } + ) + }) + }) + + describe('setServerId', function() { + beforeEach(function() { + this.response = 'dsadsakj' + this.ClsiCookieManager._parseServerIdFromResponse = sinon + .stub() + .returns('clsi-8') + return this.redisMulti.exec.callsArgWith(0) + }) + + it('should set the server id with a ttl', function(done) { + return this.ClsiCookieManager.setServerId( + this.project_id, + this.response, + err => { + this.redisMulti.set + .calledWith(`clsiserver:${this.project_id}`, 'clsi-8') + .should.equal(true) + this.redisMulti.expire + .calledWith( + `clsiserver:${this.project_id}`, + this.settings.clsiCookie.ttl + ) + .should.equal(true) + return done() + } + ) + }) + + it('should return the server id', function(done) { + return this.ClsiCookieManager.setServerId( + this.project_id, + this.response, + (err, serverId) => { + serverId.should.equal('clsi-8') + return done() + } + ) + }) + + it('should not set the server id if clsiCookies are not enabled', function(done) { + delete this.settings.clsiCookie.key + this.ClsiCookieManager = SandboxedModule.require(modulePath, { + requires: this.requires + })() + return this.ClsiCookieManager.setServerId( + this.project_id, + this.response, + (err, serverId) => { + this.redisMulti.exec.called.should.equal(false) + return done() + } + ) + }) + + it('should not set the server id there is no server id in the response', function(done) { + this.ClsiCookieManager._parseServerIdFromResponse = sinon + .stub() + .returns(null) + return this.ClsiCookieManager.setServerId( + this.project_id, + this.response, + (err, serverId) => { + this.redisMulti.exec.called.should.equal(false) + return done() + } + ) + }) + + return it('should also set in the secondary if secondary redis is enabled', function(done) { + this.redisSecondaryMulti = { + set: sinon.stub(), + expire: sinon.stub(), + exec: sinon.stub() + } + this.redis_secondary = { multi: () => this.redisSecondaryMulti } + this.settings.redis.clsi_cookie_secondary = {} + this.RedisWrapper.client = sinon.stub() + this.RedisWrapper.client.withArgs('clsi_cookie').returns(this.redis) + this.RedisWrapper.client + .withArgs('clsi_cookie_secondary') + .returns(this.redis_secondary) + this.ClsiCookieManager = SandboxedModule.require(modulePath, { + requires: this.requires + })() + this.ClsiCookieManager._parseServerIdFromResponse = sinon + .stub() + .returns('clsi-8') + return this.ClsiCookieManager.setServerId( + this.project_id, + this.response, + (err, serverId) => { + this.redisSecondaryMulti.set + .calledWith(`clsiserver:${this.project_id}`, 'clsi-8') + .should.equal(true) + this.redisSecondaryMulti.expire + .calledWith( + `clsiserver:${this.project_id}`, + this.settings.clsiCookie.ttl + ) + .should.equal(true) + return done() + } + ) + }) + }) + + return describe('getCookieJar', function() { + beforeEach(function() { + return (this.ClsiCookieManager._getServerId = sinon + .stub() + .callsArgWith(1, null, 'clsi-11')) + }) + + it('should return a jar with the cookie set populated from redis', function(done) { + return this.ClsiCookieManager.getCookieJar( + this.project_id, + (err, jar) => { + jar._jar.store.idx['clsi.example.com']['/'][ + this.settings.clsiCookie.key + ].key.should.equal + jar._jar.store.idx['clsi.example.com']['/'][ + this.settings.clsiCookie.key + ].value.should.equal('clsi-11') + return done() + } + ) + }) + + return it('should return empty cookie jar if clsiCookies are not enabled', function(done) { + delete this.settings.clsiCookie.key + this.ClsiCookieManager = SandboxedModule.require(modulePath, { + requires: this.requires + })() + return this.ClsiCookieManager.getCookieJar(this.project_id, function( + err, + jar + ) { + assert.deepEqual(jar, realRequst.jar()) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Compile/ClsiFormatCheckerTests.js b/services/web/test/unit/src/Compile/ClsiFormatCheckerTests.js new file mode 100644 index 0000000000..1020faac2c --- /dev/null +++ b/services/web/test/unit/src/Compile/ClsiFormatCheckerTests.js @@ -0,0 +1,247 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Compile/ClsiFormatChecker.js' +const SandboxedModule = require('sandboxed-module') + +describe('ClsiFormatChecker', function() { + beforeEach(function() { + this.ClsiFormatChecker = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = { compileBodySizeLimitMb: 5 }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }) + } + }) + return (this.project_id = 'project-id') + }) + + return describe('checkRecoursesForProblems', function() { + beforeEach(function() { + return (this.resources = [ + { + path: 'main.tex', + content: 'stuff' + }, + { + path: 'chapters/chapter1', + content: 'other stuff' + }, + { + path: 'stuff/image/image.png', + url: `http:somewhere.com/project/${ + this.project_id + }/file/1234124321312`, + modified: 'more stuff' + } + ]) + }) + + it('should call _checkForDuplicatePaths and _checkForConflictingPaths', function(done) { + this.ClsiFormatChecker._checkForConflictingPaths = sinon + .stub() + .callsArgWith(1, null) + this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon + .stub() + .callsArgWith(1) + return this.ClsiFormatChecker.checkRecoursesForProblems( + this.resources, + (err, problems) => { + this.ClsiFormatChecker._checkForConflictingPaths.called.should.equal( + true + ) + this.ClsiFormatChecker._checkDocsAreUnderSizeLimit.called.should.equal( + true + ) + return done() + } + ) + }) + + it('should remove undefined errors', function(done) { + this.ClsiFormatChecker._checkForConflictingPaths = sinon + .stub() + .callsArgWith(1, null, []) + this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon + .stub() + .callsArgWith(1, null, {}) + return this.ClsiFormatChecker.checkRecoursesForProblems( + this.resources, + (err, problems) => { + expect(problems).to.not.exist + expect(problems).to.not.exist + return done() + } + ) + }) + + it('should keep populated arrays', function(done) { + this.ClsiFormatChecker._checkForConflictingPaths = sinon + .stub() + .callsArgWith(1, null, [{ path: 'somewhere/main.tex' }]) + this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon + .stub() + .callsArgWith(1, null, {}) + return this.ClsiFormatChecker.checkRecoursesForProblems( + this.resources, + (err, problems) => { + problems.conflictedPaths[0].path.should.equal('somewhere/main.tex') + expect(problems.sizeCheck).to.not.exist + return done() + } + ) + }) + + it('should keep populated object', function(done) { + this.ClsiFormatChecker._checkForConflictingPaths = sinon + .stub() + .callsArgWith(1, null, []) + this.ClsiFormatChecker._checkDocsAreUnderSizeLimit = sinon + .stub() + .callsArgWith(1, null, { + resources: [{ 'a.tex': 'a.tex' }, { 'b.tex': 'b.tex' }], + totalSize: 1000000 + }) + return this.ClsiFormatChecker.checkRecoursesForProblems( + this.resources, + (err, problems) => { + problems.sizeCheck.resources.length.should.equal(2) + problems.sizeCheck.totalSize.should.equal(1000000) + expect(problems.conflictedPaths).to.not.exist + return done() + } + ) + }) + + describe('_checkForConflictingPaths', function() { + beforeEach(function() { + this.resources.push({ + path: 'chapters/chapter1.tex', + content: 'other stuff' + }) + + return this.resources.push({ + path: 'chapters.tex', + content: 'other stuff' + }) + }) + + it('should flag up when a nested file has folder with same subpath as file elsewhere', function(done) { + this.resources.push({ + path: 'stuff/image', + url: 'http://somwhere.com' + }) + + return this.ClsiFormatChecker._checkForConflictingPaths( + this.resources, + function(err, conflictPathErrors) { + conflictPathErrors.length.should.equal(1) + conflictPathErrors[0].path.should.equal('stuff/image') + return done() + } + ) + }) + + it('should flag up when a root level file has folder with same subpath as file elsewhere', function(done) { + this.resources.push({ + path: 'stuff', + content: 'other stuff' + }) + + return this.ClsiFormatChecker._checkForConflictingPaths( + this.resources, + function(err, conflictPathErrors) { + conflictPathErrors.length.should.equal(1) + conflictPathErrors[0].path.should.equal('stuff') + return done() + } + ) + }) + + return it('should not flag up when the file is a substring of a path', function(done) { + this.resources.push({ + path: 'stuf', + content: 'other stuff' + }) + + return this.ClsiFormatChecker._checkForConflictingPaths( + this.resources, + function(err, conflictPathErrors) { + conflictPathErrors.length.should.equal(0) + return done() + } + ) + }) + }) + + return describe('_checkDocsAreUnderSizeLimit', function() { + it('should error when there is more than 5mb of data', function(done) { + this.resources.push({ + path: 'massive.tex', + content: require('crypto') + .randomBytes(1000 * 1000 * 5) + .toString('hex') + }) + + while (this.resources.length < 20) { + this.resources.push({ + path: 'chapters/chapter1.tex', + url: 'http://somwhere.com' + }) + } + + return this.ClsiFormatChecker._checkDocsAreUnderSizeLimit( + this.resources, + function(err, sizeError) { + sizeError.totalSize.should.equal(10000016) + sizeError.resources.length.should.equal(10) + sizeError.resources[0].path.should.equal('massive.tex') + sizeError.resources[0].size.should.equal(1000 * 1000 * 10) + return done() + } + ) + }) + + return it('should return nothing when project is correct size', function(done) { + this.resources.push({ + path: 'massive.tex', + content: require('crypto') + .randomBytes(1000 * 1000 * 1) + .toString('hex') + }) + + while (this.resources.length < 20) { + this.resources.push({ + path: 'chapters/chapter1.tex', + url: 'http://somwhere.com' + }) + } + + return this.ClsiFormatChecker._checkDocsAreUnderSizeLimit( + this.resources, + function(err, sizeError) { + expect(sizeError).to.not.exist + return done() + } + ) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Compile/ClsiManagerTests.js b/services/web/test/unit/src/Compile/ClsiManagerTests.js new file mode 100644 index 0000000000..6e149a42ce --- /dev/null +++ b/services/web/test/unit/src/Compile/ClsiManagerTests.js @@ -0,0 +1,1034 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Compile/ClsiManager.js' +const SandboxedModule = require('sandboxed-module') + +describe('ClsiManager', function() { + beforeEach(function() { + let Timer + this.jar = { cookie: 'stuff' } + this.ClsiCookieManager = { + getCookieJar: sinon.stub().callsArgWith(1, null, this.jar), + setServerId: sinon.stub().callsArgWith(2), + _getServerId: sinon.stub() + } + this.ClsiStateManager = { + computeHash: sinon.stub().callsArgWith(2, null, '01234567890abcdef') + } + this.ClsiFormatChecker = { + checkRecoursesForProblems: sinon.stub().callsArgWith(1) + } + this.ClsiManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = { + apis: { + filestore: { + url: 'filestore.example.com', + secret: 'secret' + }, + clsi: { + url: 'http://clsi.example.com' + }, + clsi_priority: { + url: 'https://clsipremium.example.com' + } + } + }), + '../../models/Project': { + Project: (this.Project = {}) + }, + '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../DocumentUpdater/DocumentUpdaterHandler': (this.DocumentUpdaterHandler = { + getProjectDocsIfMatch: sinon.stub().callsArgWith(2, null, null) + }), + './ClsiCookieManager': () => this.ClsiCookieManager, + './ClsiStateManager': this.ClsiStateManager, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err: sinon.stub(), + warn: sinon.stub() + }), + request: (this.request = sinon.stub()), + './ClsiFormatChecker': this.ClsiFormatChecker, + 'metrics-sharelatex': (this.Metrics = { + Timer: (Timer = (function() { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()), + inc: sinon.stub() + }) + } + }) + this.project_id = 'project-id' + this.user_id = 'user-id' + return (this.callback = sinon.stub()) + }) + + describe('sendRequest', function() { + beforeEach(function() { + this.ClsiManager._buildRequest = sinon + .stub() + .callsArgWith(2, null, (this.request = 'mock-request')) + return this.ClsiCookieManager._getServerId.callsArgWith(1, null, 'clsi3') + }) + + describe('with a successful compile', function() { + beforeEach(function() { + this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { + compile: { + status: (this.status = 'success'), + outputFiles: [ + { + url: `${this.settings.apis.clsi.url}/project/${ + this.project_id + }/user/${this.user_id}/build/1234/output/output.pdf`, + path: 'output.pdf', + type: 'pdf', + build: 1234 + }, + { + url: `${this.settings.apis.clsi.url}/project/${ + this.project_id + }/user/${this.user_id}/build/1234/output/output.log`, + path: 'output.log', + type: 'log', + build: 1234 + } + ] + } + }) + return this.ClsiManager.sendRequest( + this.project_id, + this.user_id, + { compileGroup: 'standard' }, + this.callback + ) + }) + + it('should build the request', function() { + return this.ClsiManager._buildRequest + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should send the request to the CLSI', function() { + return this.ClsiManager._postToClsi + .calledWith(this.project_id, this.user_id, this.request, 'standard') + .should.equal(true) + }) + + return it('should call the callback with the status and output files', function() { + const outputFiles = [ + { + url: `/project/${this.project_id}/user/${ + this.user_id + }/build/1234/output/output.pdf`, + path: 'output.pdf', + type: 'pdf', + build: 1234 + }, + { + url: `/project/${this.project_id}/user/${ + this.user_id + }/build/1234/output/output.log`, + path: 'output.log', + type: 'log', + build: 1234 + } + ] + return this.callback + .calledWith(null, this.status, outputFiles) + .should.equal(true) + }) + }) + + describe('with a failed compile', function() { + beforeEach(function() { + this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { + compile: { + status: (this.status = 'failure') + } + }) + return this.ClsiManager.sendRequest( + this.project_id, + this.user_id, + {}, + this.callback + ) + }) + + return it('should call the callback with a failure status', function() { + return this.callback.calledWith(null, this.status).should.equal(true) + }) + }) + + describe('with a sync conflict', function() { + beforeEach(function() { + this.ClsiManager.sendRequestOnce = sinon.stub() + this.ClsiManager.sendRequestOnce + .withArgs(this.project_id, this.user_id, { syncType: 'full' }) + .callsArgWith(3, null, (this.status = 'success')) + this.ClsiManager.sendRequestOnce + .withArgs(this.project_id, this.user_id, {}) + .callsArgWith(3, null, 'conflict') + return this.ClsiManager.sendRequest( + this.project_id, + this.user_id, + {}, + this.callback + ) + }) + + it('should call the sendRequestOnce method twice', function() { + return this.ClsiManager.sendRequestOnce.calledTwice.should.equal(true) + }) + + it('should call the sendRequestOnce method with syncType:full', function() { + return this.ClsiManager.sendRequestOnce + .calledWith(this.project_id, this.user_id, { syncType: 'full' }) + .should.equal(true) + }) + + it('should call the sendRequestOnce method without syncType:full', function() { + return this.ClsiManager.sendRequestOnce + .calledWith(this.project_id, this.user_id, {}) + .should.equal(true) + }) + + return it('should call the callback with a success status', function() { + return this.callback.calledWith(null, this.status).should.equal(true) + }) + }) + + return describe('when the resources fail the precompile check', function() { + beforeEach(function() { + this.ClsiFormatChecker.checkRecoursesForProblems = sinon + .stub() + .callsArgWith(1, new Error('failed')) + this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { + compile: { + status: (this.status = 'failure') + } + }) + return this.ClsiManager.sendRequest( + this.project_id, + this.user_id, + {}, + this.callback + ) + }) + + it('should call the callback only once', function() { + return this.callback.calledOnce.should.equal(true) + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWithExactly(new Error('failed')) + .should.equal(true) + }) + }) + }) + + describe('sendExternalRequest', function() { + beforeEach(function() { + this.submission_id = 'submission-id' + this.clsi_request = 'mock-request' + return this.ClsiCookieManager._getServerId.callsArgWith(1, null, 'clsi3') + }) + + describe('with a successful compile', function() { + beforeEach(function() { + this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { + compile: { + status: (this.status = 'success'), + outputFiles: [ + { + url: `${this.settings.apis.clsi.url}/project/${ + this.submission_id + }/build/1234/output/output.pdf`, + path: 'output.pdf', + type: 'pdf', + build: 1234 + }, + { + url: `${this.settings.apis.clsi.url}/project/${ + this.submission_id + }/build/1234/output/output.log`, + path: 'output.log', + type: 'log', + build: 1234 + } + ] + } + }) + return this.ClsiManager.sendExternalRequest( + this.submission_id, + this.clsi_request, + { compileGroup: 'standard' }, + this.callback + ) + }) + + it('should send the request to the CLSI', function() { + return this.ClsiManager._postToClsi + .calledWith(this.submission_id, null, this.clsi_request, 'standard') + .should.equal(true) + }) + + return it('should call the callback with the status and output files', function() { + const outputFiles = [ + { + url: `/project/${this.submission_id}/build/1234/output/output.pdf`, + path: 'output.pdf', + type: 'pdf', + build: 1234 + }, + { + url: `/project/${this.submission_id}/build/1234/output/output.log`, + path: 'output.log', + type: 'log', + build: 1234 + } + ] + return this.callback + .calledWith(null, this.status, outputFiles) + .should.equal(true) + }) + }) + + describe('with a failed compile', function() { + beforeEach(function() { + this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { + compile: { + status: (this.status = 'failure') + } + }) + return this.ClsiManager.sendExternalRequest( + this.submission_id, + this.clsi_request, + {}, + this.callback + ) + }) + + return it('should call the callback with a failure status', function() { + return this.callback.calledWith(null, this.status).should.equal(true) + }) + }) + + return describe('when the resources fail the precompile check', function() { + beforeEach(function() { + this.ClsiFormatChecker.checkRecoursesForProblems = sinon + .stub() + .callsArgWith(1, new Error('failed')) + this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { + compile: { + status: (this.status = 'failure') + } + }) + return this.ClsiManager.sendExternalRequest( + this.submission_id, + this.clsi_request, + {}, + this.callback + ) + }) + + it('should call the callback only once', function() { + return this.callback.calledOnce.should.equal(true) + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWithExactly(new Error('failed')) + .should.equal(true) + }) + }) + }) + + describe('deleteAuxFiles', function() { + beforeEach(function() { + this.ClsiManager._makeRequest = sinon.stub().callsArg(2) + return (this.DocumentUpdaterHandler.clearProjectState = sinon + .stub() + .callsArg(1)) + }) + + return describe('with the standard compileGroup', function() { + beforeEach(function() { + return this.ClsiManager.deleteAuxFiles( + this.project_id, + this.user_id, + { compileGroup: 'standard' }, + this.callback + ) + }) + + it('should call the delete method in the standard CLSI', function() { + return this.ClsiManager._makeRequest + .calledWith(this.project_id, { + method: 'DELETE', + url: `${this.settings.apis.clsi.url}/project/${ + this.project_id + }/user/${this.user_id}` + }) + .should.equal(true) + }) + + it('should clear the project state from the docupdater', function() { + return this.DocumentUpdaterHandler.clearProjectState + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('_buildRequest', function() { + beforeEach(function() { + this.project = { + _id: this.project_id, + compiler: (this.compiler = 'latex'), + rootDoc_id: 'mock-doc-id-1', + imageName: (this.image = 'mock-image-name') + } + + this.docs = { + '/main.tex': (this.doc_1 = { + name: 'main.tex', + _id: 'mock-doc-id-1', + lines: ['Hello', 'world'] + }), + '/chapters/chapter1.tex': (this.doc_2 = { + name: 'chapter1.tex', + _id: 'mock-doc-id-2', + lines: ['Chapter 1'] + }) + } + + this.files = { + '/images/image.png': (this.file_1 = { + name: 'image.png', + _id: 'mock-file-id-1', + created: new Date() + }) + } + + this.Project.findById = sinon.stub().callsArgWith(2, null, this.project) + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + this.ProjectEntityHandler.getAllFiles = sinon + .stub() + .callsArgWith(1, null, this.files) + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project) + return (this.DocumentUpdaterHandler.flushProjectToMongo = sinon + .stub() + .callsArgWith(1, null)) + }) + + describe('with a valid project', function() { + beforeEach(function(done) { + return this.ClsiManager._buildRequest( + this.project_id, + { timeout: 100 }, + (error, request) => { + this.request = request + return done() + } + ) + }) + + it('should get the project with the required fields', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { + compiler: 1, + rootDoc_id: 1, + imageName: 1, + rootFolder: 1 + }) + .should.equal(true) + }) + + it('should flush the project to the database', function() { + return this.DocumentUpdaterHandler.flushProjectToMongo + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should get all the docs', function() { + return this.ProjectEntityHandler.getAllDocs + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should get all the files', function() { + return this.ProjectEntityHandler.getAllFiles + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should build up the CLSI request', function() { + return expect(this.request).to.deep.equal({ + compile: { + options: { + compiler: this.compiler, + timeout: 100, + imageName: this.image, + draft: false, + check: undefined, + syncType: undefined, // "full" + syncState: undefined + }, // "01234567890abcdef" + rootResourcePath: 'main.tex', + resources: [ + { + path: 'main.tex', + content: this.doc_1.lines.join('\n') + }, + { + path: 'chapters/chapter1.tex', + content: this.doc_2.lines.join('\n') + }, + { + path: 'images/image.png', + url: `${this.settings.apis.filestore.url}/project/${ + this.project_id + }/file/${this.file_1._id}`, + modified: this.file_1.created.getTime() + } + ] + } + }) + }) + }) + + describe('with the incremental compile option', function() { + beforeEach(function(done) { + this.ClsiStateManager.computeHash = sinon + .stub() + .callsArgWith( + 2, + null, + (this.project_state_hash = '01234567890abcdef') + ) + this.DocumentUpdaterHandler.getProjectDocsIfMatch = sinon + .stub() + .callsArgWith(2, null, [ + { _id: this.doc_1._id, lines: this.doc_1.lines, v: 123 } + ]) + this.ProjectEntityHandler.getAllDocPathsFromProject = sinon + .stub() + .callsArgWith(1, null, { 'mock-doc-id-1': 'main.tex' }) + return this.ClsiManager._buildRequest( + this.project_id, + { timeout: 100, incrementalCompilesEnabled: true }, + (error, request) => { + this.request = request + return done() + } + ) + }) + + it('should get the project with the required fields', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { + compiler: 1, + rootDoc_id: 1, + imageName: 1, + rootFolder: 1 + }) + .should.equal(true) + }) + + it('should not explicitly flush the project to the database', function() { + return this.DocumentUpdaterHandler.flushProjectToMongo + .calledWith(this.project_id) + .should.equal(false) + }) + + it('should get only the live docs from the docupdater with a background flush in docupdater', function() { + return this.DocumentUpdaterHandler.getProjectDocsIfMatch + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should not get any of the files', function() { + return this.ProjectEntityHandler.getAllFiles.called.should.equal(false) + }) + + it('should build up the CLSI request', function() { + return expect(this.request).to.deep.equal({ + compile: { + options: { + compiler: this.compiler, + timeout: 100, + imageName: this.image, + draft: false, + check: undefined, + syncType: 'incremental', + syncState: '01234567890abcdef' + }, + rootResourcePath: 'main.tex', + resources: [ + { + path: 'main.tex', + content: this.doc_1.lines.join('\n') + } + ] + } + }) + }) + + return describe('when the root doc is set and not in the docupdater', function() { + beforeEach(function(done) { + this.ClsiStateManager.computeHash = sinon + .stub() + .callsArgWith( + 2, + null, + (this.project_state_hash = '01234567890abcdef') + ) + this.DocumentUpdaterHandler.getProjectDocsIfMatch = sinon + .stub() + .callsArgWith(2, null, [ + { _id: this.doc_1._id, lines: this.doc_1.lines, v: 123 } + ]) + this.ProjectEntityHandler.getAllDocPathsFromProject = sinon + .stub() + .callsArgWith(1, null, { + 'mock-doc-id-1': 'main.tex', + 'mock-doc-id-2': '/chapters/chapter1.tex' + }) + return this.ClsiManager._buildRequest( + this.project_id, + { + timeout: 100, + incrementalCompilesEnabled: true, + rootDoc_id: 'mock-doc-id-2' + }, + (error, request) => { + this.request = request + return done() + } + ) + }) + + return it('should still change the root path', function() { + return this.request.compile.rootResourcePath.should.equal( + 'chapters/chapter1.tex' + ) + }) + }) + }) + + describe('when root doc override is valid', function() { + beforeEach(function(done) { + return this.ClsiManager._buildRequest( + this.project_id, + { rootDoc_id: 'mock-doc-id-2' }, + (error, request) => { + this.request = request + return done() + } + ) + }) + + return it('should change root path', function() { + return this.request.compile.rootResourcePath.should.equal( + 'chapters/chapter1.tex' + ) + }) + }) + + describe('when root doc override is invalid', function() { + beforeEach(function(done) { + return this.ClsiManager._buildRequest( + this.project_id, + { rootDoc_id: 'invalid-id' }, + (error, request) => { + this.request = request + return done() + } + ) + }) + + return it('should fallback to default root doc', function() { + return this.request.compile.rootResourcePath.should.equal('main.tex') + }) + }) + + describe('when the project has an invalid compiler', function() { + beforeEach(function(done) { + this.project.compiler = 'context' + return this.ClsiManager._buildRequest( + this.project, + null, + (error, request) => { + this.request = request + return done() + } + ) + }) + + return it('should set the compiler to pdflatex', function() { + return this.request.compile.options.compiler.should.equal('pdflatex') + }) + }) + + describe('when there is no valid root document', function() { + beforeEach(function(done) { + this.project.rootDoc_id = 'not-valid' + return this.ClsiManager._buildRequest( + this.project, + null, + (error, request) => { + this.error = error + this.request = request + return done() + } + ) + }) + + return it('should set to main.tex', function() { + return this.request.compile.rootResourcePath.should.equal('main.tex') + }) + }) + + describe('when there is no valid root document and no main.tex document', function() { + beforeEach(function() { + this.project.rootDoc_id = 'not-valid' + this.docs = { + '/other.tex': (this.doc_1 = { + name: 'other.tex', + _id: 'mock-doc-id-1', + lines: ['Hello', 'world'] + }), + '/chapters/chapter1.tex': (this.doc_2 = { + name: 'chapter1.tex', + _id: 'mock-doc-id-2', + lines: ['Chapter 1'] + }) + } + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + return this.ClsiManager._buildRequest(this.project, null, this.callback) + }) + + return it('should report an error', function() { + return this.callback + .calledWith(new Error('no main file specified')) + .should.equal(true) + }) + }) + + describe('when there is no valid root document and a single document which is not main.tex', function() { + beforeEach(function(done) { + this.project.rootDoc_id = 'not-valid' + this.docs = { + '/other.tex': (this.doc_1 = { + name: 'other.tex', + _id: 'mock-doc-id-1', + lines: ['Hello', 'world'] + }) + } + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + return this.ClsiManager._buildRequest( + this.project, + null, + (error, request) => { + this.error = error + this.request = request + return done() + } + ) + }) + + return it('should set io to the only file', function() { + return this.request.compile.rootResourcePath.should.equal('other.tex') + }) + }) + + return describe('with the draft option', () => + it('should add the draft option into the request', function(done) { + return this.ClsiManager._buildRequest( + this.project_id, + { timeout: 100, draft: true }, + (error, request) => { + request.compile.options.draft.should.equal(true) + return done() + } + ) + })) + }) + + describe('_postToClsi', function() { + beforeEach(function() { + return (this.req = { mock: 'req' }) + }) + + describe('successfully', function() { + beforeEach(function() { + this.ClsiManager._makeRequest = sinon + .stub() + .callsArgWith( + 2, + null, + { statusCode: 204 }, + (this.body = { mock: 'foo' }) + ) + return this.ClsiManager._postToClsi( + this.project_id, + this.user_id, + this.req, + 'standard', + this.callback + ) + }) + + it('should send the request to the CLSI', function() { + const url = `${this.settings.apis.clsi.url}/project/${ + this.project_id + }/user/${this.user_id}/compile` + return this.ClsiManager._makeRequest + .calledWith(this.project_id, { + method: 'POST', + url, + json: this.req + }) + .should.equal(true) + }) + + return it('should call the callback with the body and no error', function() { + return this.callback.calledWith(null, this.body).should.equal(true) + }) + }) + + return describe('when the CLSI returns an error', function() { + beforeEach(function() { + this.ClsiManager._makeRequest = sinon + .stub() + .callsArgWith( + 2, + null, + { statusCode: 500 }, + (this.body = { mock: 'foo' }) + ) + return this.ClsiManager._postToClsi( + this.project_id, + this.user_id, + this.req, + 'standard', + this.callback + ) + }) + + return it('should call the callback with the body and the error', function() { + return this.callback + .calledWith( + new Error('CLSI returned non-success code: 500'), + this.body + ) + .should.equal(true) + }) + }) + }) + + describe('wordCount', function() { + beforeEach(function() { + this.ClsiManager._makeRequest = sinon + .stub() + .callsArgWith( + 2, + null, + { statusCode: 200 }, + (this.body = { mock: 'foo' }) + ) + return (this.ClsiManager._buildRequest = sinon.stub().callsArgWith( + 2, + null, + (this.req = { + compile: { rootResourcePath: 'rootfile.text', options: {} } + }) + )) + }) + + describe('with root file', function() { + beforeEach(function() { + return this.ClsiManager.wordCount( + this.project_id, + this.user_id, + false, + {}, + this.callback + ) + }) + + it('should call wordCount with root file', function() { + return this.ClsiManager._makeRequest + .calledWith(this.project_id, { + method: 'GET', + url: `http://clsi.example.com/project/${this.project_id}/user/${ + this.user_id + }/wordcount`, + qs: { file: 'rootfile.text', image: undefined } + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('with param file', function() { + beforeEach(function() { + return this.ClsiManager.wordCount( + this.project_id, + this.user_id, + 'main.tex', + {}, + this.callback + ) + }) + + return it('should call wordCount with param file', function() { + return this.ClsiManager._makeRequest + .calledWith(this.project_id, { + method: 'GET', + url: `http://clsi.example.com/project/${this.project_id}/user/${ + this.user_id + }/wordcount`, + qs: { file: 'main.tex', image: undefined } + }) + .should.equal(true) + }) + }) + + return describe('with image', function() { + beforeEach(function() { + this.req.compile.options.imageName = this.image = + 'example.com/mock/image' + return this.ClsiManager.wordCount( + this.project_id, + this.user_id, + 'main.tex', + {}, + this.callback + ) + }) + + return it('should call wordCount with file and image', function() { + return this.ClsiManager._makeRequest + .calledWith(this.project_id, { + method: 'GET', + url: `http://clsi.example.com/project/${this.project_id}/user/${ + this.user_id + }/wordcount`, + qs: { file: 'main.tex', image: this.image } + }) + .should.equal(true) + }) + }) + }) + + describe('_makeRequest', function() { + beforeEach(function() { + this.response = { there: 'something' } + this.request.callsArgWith(1, null, this.response) + return (this.opts = { + method: 'SOMETHIGN', + url: 'http://a place on the web' + }) + }) + + it('should process a request with a cookie jar', function(done) { + return this.ClsiManager._makeRequest(this.project_id, this.opts, () => { + const args = this.request.args[0] + args[0].method.should.equal(this.opts.method) + args[0].url.should.equal(this.opts.url) + args[0].jar.should.equal(this.jar) + return done() + }) + }) + + return it('should set the cookie again on response as it might have changed', function(done) { + return this.ClsiManager._makeRequest(this.project_id, this.opts, () => { + this.ClsiCookieManager.setServerId + .calledWith(this.project_id, this.response) + .should.equal(true) + return done() + }) + }) + }) + + return describe('_makeGoogleCloudRequest', function() { + beforeEach(function() { + this.settings.apis.clsi_new = { url: 'https://compiles.somewhere.test' } + this.response = { there: 'something' } + this.request.callsArgWith(1, null, this.response) + return (this.opts = { + url: this.ClsiManager._getCompilerUrl(null, this.project_id) + }) + }) + + it('should change the domain on the url', function(done) { + return this.ClsiManager._makeNewBackendRequest( + this.project_id, + this.opts, + () => { + const args = this.request.args[0] + args[0].url.should.equal( + `https://compiles.somewhere.test/project/${this.project_id}` + ) + return done() + } + ) + }) + + return it('should not make a request if there is not clsi_new url', function(done) { + this.settings.apis.clsi_new = undefined + return this.ClsiManager._makeNewBackendRequest( + this.project_id, + this.opts, + err => { + expect(err).to.equal(undefined) + this.request.callCount.should.equal(0) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Compile/ClsiStateManagerTests.js b/services/web/test/unit/src/Compile/ClsiStateManagerTests.js new file mode 100644 index 0000000000..6ec7936c79 --- /dev/null +++ b/services/web/test/unit/src/Compile/ClsiStateManagerTests.js @@ -0,0 +1,302 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Compile/ClsiStateManager.js' +const SandboxedModule = require('sandboxed-module') + +describe('ClsiStateManager', function() { + beforeEach(function() { + this.ClsiStateManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = {}), + '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }) + } + }) + this.project = 'project' + this.options = { draft: true, isAutoCompile: false } + return (this.callback = sinon.stub()) + }) + + return describe('computeHash', function() { + beforeEach(function(done) { + this.docs = [ + { path: '/main.tex', doc: { _id: 'doc-id-1' } }, + { path: '/folder/sub.tex', doc: { _id: 'doc-id-2' } } + ] + this.files = [ + { + path: '/figure.pdf', + file: { _id: 'file-id-1', rev: 123, created: 'aaaaaa' } + }, + { + path: '/folder/fig2.pdf', + file: { _id: 'file-id-2', rev: 456, created: 'bbbbbb' } + } + ] + this.ProjectEntityHandler.getAllEntitiesFromProject = sinon + .stub() + .callsArgWith(1, null, this.docs, this.files) + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash0 = hash + return done() + } + ) + }) + + describe('with a sample project', function() { + beforeEach(function() { + return this.ClsiStateManager.computeHash( + this.project, + this.options, + this.callback + ) + }) + + return it('should call the callback with a hash value', function() { + return this.callback + .calledWith(null, '21b1ab73aa3892bec452baf8ffa0956179e1880f') + .should.equal(true) + }) + }) + + describe('when the files and docs are in a different order', function() { + beforeEach(function() { + ;[this.docs[0], this.docs[1]] = Array.from([this.docs[1], this.docs[0]]) + ;[this.files[0], this.files[1]] = Array.from([ + this.files[1], + this.files[0] + ]) + return this.ClsiStateManager.computeHash( + this.project, + this.options, + this.callback + ) + }) + + return it('should call the callback with the same hash value', function() { + return this.callback.calledWith(null, this.hash0).should.equal(true) + }) + }) + + describe('when a doc is renamed', function() { + beforeEach(function(done) { + this.docs[0].path = '/new.tex' + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe('when a file is renamed', function() { + beforeEach(function(done) { + this.files[0].path = '/newfigure.pdf' + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe('when a doc is added', function() { + beforeEach(function(done) { + this.docs.push({ path: '/newdoc.tex', doc: { _id: 'newdoc-id' } }) + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe('when a file is added', function() { + beforeEach(function(done) { + this.files.push({ + path: '/newfile.tex', + file: { _id: 'newfile-id', rev: 123 } + }) + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe('when a doc is removed', function() { + beforeEach(function(done) { + this.docs.pop() + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe('when a file is removed', function() { + beforeEach(function(done) { + this.files.pop() + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe("when a file's revision is updated", function() { + beforeEach(function(done) { + this.files[0].file.rev++ + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe("when a file's date is updated", function() { + beforeEach(function(done) { + this.files[0].file.created = 'zzzzzz' + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + describe('when the compile options are changed', function() { + beforeEach(function(done) { + this.options.draft = !this.options.draft + return this.ClsiStateManager.computeHash( + this.project, + this.options, + (err, hash) => { + this.hash1 = hash + return done() + } + ) + }) + + return it('should call the callback with a different hash value', function() { + return this.callback + .neverCalledWith(null, this.hash0) + .should.equal(true) + }) + }) + + return describe('when the isAutoCompile option is changed', function() { + beforeEach(function() { + this.options.isAutoCompile = !this.options.isAutoCompile + return this.ClsiStateManager.computeHash( + this.project, + this.options, + this.callback + ) + }) + + return it('should call the callback with the same hash value', function() { + return this.callback.calledWith(null, this.hash0).should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Compile/CompileControllerTests.js b/services/web/test/unit/src/Compile/CompileControllerTests.js new file mode 100644 index 0000000000..3584fa87c4 --- /dev/null +++ b/services/web/test/unit/src/Compile/CompileControllerTests.js @@ -0,0 +1,738 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { assert } = require('chai') +const { expect } = chai +const modulePath = '../../../../app/src/Features/Compile/CompileController.js' +const SandboxedModule = require('sandboxed-module') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') + +describe('CompileController', function() { + beforeEach(function() { + this.user_id = 'wat' + this.user = { + _id: this.user_id, + email: 'user@example.com', + features: { + compileGroup: 'premium', + compileTimeout: 100 + } + } + this.CompileManager = { compile: sinon.stub() } + this.ClsiManager = {} + this.UserGetter = { getUser: sinon.stub() } + this.RateLimiter = { addCount: sinon.stub() } + this.settings = { + apis: { + clsi: { + url: 'clsi.example.com' + }, + clsi_priority: { + url: 'clsi-priority.example.com' + } + }, + defaultFeatures: { + compileGroup: 'standard', + compileTimeout: 60 + } + } + this.jar = { cookie: 'stuff' } + this.ClsiCookieManager = { + getCookieJar: sinon.stub().callsArgWith(1, null, this.jar) + } + this.AuthenticationController = { + getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user), + getLoggedInUserId: sinon.stub().returns(this.user_id), + getSessionUser: sinon.stub().returns(this.user), + isUserLoggedIn: sinon.stub().returns(true) + } + this.CompileController = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + request: (this.request = sinon.stub()), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + 'metrics-sharelatex': (this.Metrics = { inc: sinon.stub() }), + './CompileManager': this.CompileManager, + '../User/UserGetter': this.UserGetter, + './ClsiManager': this.ClsiManager, + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../../infrastructure/RateLimiter': this.RateLimiter, + './ClsiCookieManager': () => this.ClsiCookieManager + } + }) + this.project_id = 'project-id' + this.next = sinon.stub() + this.req = new MockRequest() + return (this.res = new MockResponse()) + }) + + describe('compile', function() { + beforeEach(function() { + this.req.params = { Project_id: this.project_id } + this.req.session = {} + return (this.CompileManager.compile = sinon + .stub() + .callsArgWith( + 3, + null, + (this.status = 'success'), + (this.outputFiles = ['mock-output-files']) + )) + }) + + describe('when not an auto compile', function() { + beforeEach(function() { + return this.CompileController.compile(this.req, this.res, this.next) + }) + + it('should look up the user id', function() { + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should do the compile without the auto compile flag', function() { + return this.CompileManager.compile + .calledWith(this.project_id, this.user_id, { isAutoCompile: false }) + .should.equal(true) + }) + + it('should set the content-type of the response to application/json', function() { + return this.res.contentType + .calledWith('application/json') + .should.equal(true) + }) + + return it('should send a successful response reporting the status and files', function() { + this.res.statusCode.should.equal(200) + return this.res.body.should.equal( + JSON.stringify({ + status: this.status, + outputFiles: this.outputFiles + }) + ) + }) + }) + + describe('when an auto compile', function() { + beforeEach(function() { + this.req.query = { auto_compile: 'true' } + return this.CompileController.compile(this.req, this.res, this.next) + }) + + return it('should do the compile with the auto compile flag', function() { + return this.CompileManager.compile + .calledWith(this.project_id, this.user_id, { isAutoCompile: true }) + .should.equal(true) + }) + }) + + return describe('with the draft attribute', function() { + beforeEach(function() { + this.req.body = { draft: true } + return this.CompileController.compile(this.req, this.res, this.next) + }) + + return it('should do the compile without the draft compile flag', function() { + return this.CompileManager.compile + .calledWith(this.project_id, this.user_id, { + isAutoCompile: false, + draft: true + }) + .should.equal(true) + }) + }) + }) + + describe('compileSubmission', function() { + beforeEach(function() { + this.submission_id = 'sub-1234' + this.req.params = { submission_id: this.submission_id } + this.req.body = {} + return (this.ClsiManager.sendExternalRequest = sinon + .stub() + .callsArgWith( + 3, + null, + (this.status = 'success'), + (this.outputFiles = ['mock-output-files']), + (this.clsiServerId = 'mock-server-id'), + (this.validationProblems = null) + )) + }) + + it('should set the content-type of the response to application/json', function() { + this.CompileController.compileSubmission(this.req, this.res, this.next) + return this.res.contentType + .calledWith('application/json') + .should.equal(true) + }) + + it('should send a successful response reporting the status and files', function() { + this.CompileController.compileSubmission(this.req, this.res, this.next) + this.res.statusCode.should.equal(200) + return this.res.body.should.equal( + JSON.stringify({ + status: this.status, + outputFiles: this.outputFiles, + clsiServerId: 'mock-server-id', + validationProblems: null + }) + ) + }) + + describe('with compileGroup and timeout', function() { + beforeEach(function() { + this.req.body = { + compileGroup: 'special', + timeout: 600 + } + return this.CompileController.compileSubmission( + this.req, + this.res, + this.next + ) + }) + + return it('should use the supplied values', function() { + return this.ClsiManager.sendExternalRequest + .calledWith( + this.submission_id, + { compileGroup: 'special', timeout: 600 }, + { compileGroup: 'special', timeout: 600 } + ) + .should.equal(true) + }) + }) + + return describe('with other supported options but not compileGroup and timeout', function() { + beforeEach(function() { + this.req.body = { + rootResourcePath: 'main.tex', + compiler: 'lualatex', + draft: true, + check: 'validate' + } + return this.CompileController.compileSubmission( + this.req, + this.res, + this.next + ) + }) + + return it('should use the other options but default values for compileGroup and timeout', function() { + return this.ClsiManager.sendExternalRequest + .calledWith( + this.submission_id, + { + rootResourcePath: 'main.tex', + compiler: 'lualatex', + draft: true, + check: 'validate' + }, + { + rootResourcePath: 'main.tex', + compiler: 'lualatex', + draft: true, + check: 'validate', + compileGroup: 'standard', + timeout: 60 + } + ) + .should.equal(true) + }) + }) + }) + + describe('downloadPdf', function() { + beforeEach(function() { + this.req.params = { Project_id: this.project_id } + + this.req.query = { pdfng: true } + this.project = { name: 'test namè' } + return (this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project)) + }) + + describe('when downloading for embedding', function() { + beforeEach(function() { + this.CompileController.proxyToClsi = sinon.stub() + this.RateLimiter.addCount.callsArgWith(1, null, true) + return this.CompileController.downloadPdf(this.req, this.res, this.next) + }) + + it('should look up the project', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { name: 1 }) + .should.equal(true) + }) + + it('should set the content-type of the response to application/pdf', function() { + return this.res.contentType + .calledWith('application/pdf') + .should.equal(true) + }) + + it('should set the content-disposition header with a safe version of the project name', function() { + return this.res.setContentDisposition + .calledWith('', { filename: 'test_nam_.pdf' }) + .should.equal(true) + }) + + it('should increment the pdf-downloads metric', function() { + return this.Metrics.inc.calledWith('pdf-downloads').should.equal(true) + }) + + return it('should proxy the PDF from the CLSI', function() { + return this.CompileController.proxyToClsi + .calledWith( + this.project_id, + `/project/${this.project_id}/user/${ + this.user_id + }/output/output.pdf`, + this.req, + this.res, + this.next + ) + .should.equal(true) + }) + }) + + describe('when the a build-id is provided', function() { + beforeEach(function() { + this.req.params.build_id = this.buildId = '1234-5678' + this.CompileController.proxyToClsi = sinon.stub() + this.RateLimiter.addCount.callsArgWith(1, null, true) + return this.CompileController.downloadPdf(this.req, this.res, this.next) + }) + + return it('should proxy the PDF from the CLSI, with a build-id', function() { + return this.CompileController.proxyToClsi + .calledWith( + this.project_id, + `/project/${this.project_id}/user/${this.user_id}/build/${ + this.buildId + }/output/output.pdf`, + this.req, + this.res, + this.next + ) + .should.equal(true) + }) + }) + + return describe('when the pdf is not going to be used in pdfjs viewer', function() { + it('should check the rate limiter when pdfng is not set', function(done) { + this.req.query = {} + this.RateLimiter.addCount.callsArgWith(1, null, true) + this.CompileController.proxyToClsi = (project_id, url) => { + this.RateLimiter.addCount.args[0][0].throttle.should.equal(1000) + return done() + } + return this.CompileController.downloadPdf(this.req, this.res) + }) + + return it('should check the rate limiter when pdfng is false', function(done) { + this.req.query = { pdfng: false } + this.RateLimiter.addCount.callsArgWith(1, null, true) + this.CompileController.proxyToClsi = (project_id, url) => { + this.RateLimiter.addCount.args[0][0].throttle.should.equal(1000) + return done() + } + return this.CompileController.downloadPdf(this.req, this.res) + }) + }) + }) + + describe('getFileFromClsiWithoutUser', function() { + beforeEach(function() { + this.submission_id = 'sub-1234' + this.build_id = 123456 + this.file = 'project.pdf' + this.req.params = { + submission_id: this.submission_id, + build_id: this.build_id, + file: this.file + } + this.req.body = {} + this.expected_url = `/project/${this.submission_id}/build/${ + this.build_id + }/output/${this.file}` + return (this.CompileController.proxyToClsiWithLimits = sinon.stub()) + }) + + describe('without limits specified', function() { + beforeEach(function() { + return this.CompileController.getFileFromClsiWithoutUser( + this.req, + this.res, + this.next + ) + }) + + return it('should proxy to CLSI with correct URL and default limits', function() { + return this.CompileController.proxyToClsiWithLimits + .calledWith(this.submission_id, this.expected_url, { + compileGroup: 'standard' + }) + .should.equal(true) + }) + }) + + return describe('with limits specified', function() { + beforeEach(function() { + this.req.body = { compileTimeout: 600, compileGroup: 'special' } + return this.CompileController.getFileFromClsiWithoutUser( + this.req, + this.res, + this.next + ) + }) + + return it('should proxy to CLSI with correct URL and specified limits', function() { + return this.CompileController.proxyToClsiWithLimits + .calledWith(this.submission_id, this.expected_url, { + compileGroup: 'special' + }) + .should.equal(true) + }) + }) + }) + + describe('proxyToClsi', function() { + beforeEach(function() { + this.request.returns( + (this.proxy = { + pipe: sinon.stub(), + on: sinon.stub() + }) + ) + this.upstream = { + statusCode: 204, + headers: { mock: 'header' } + } + this.req.method = 'mock-method' + return (this.req.headers = { + Mock: 'Headers', + Range: '123-456', + 'If-Range': 'abcdef', + 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT' + }) + }) + + describe('old pdf viewer', function() { + describe('user with standard priority', function() { + beforeEach(function() { + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith(1, null, { compileGroup: 'standard' }) + return this.CompileController.proxyToClsi( + this.project_id, + (this.url = '/test'), + this.req, + this.res, + this.next + ) + }) + + it('should open a request to the CLSI', function() { + return this.request + .calledWith({ + jar: this.jar, + method: this.req.method, + url: `${this.settings.apis.clsi.url}${this.url}`, + timeout: 60 * 1000 + }) + .should.equal(true) + }) + + it('should pass the request on to the client', function() { + return this.proxy.pipe.calledWith(this.res).should.equal(true) + }) + + return it('should bind an error handle to the request proxy', function() { + return this.proxy.on.calledWith('error').should.equal(true) + }) + }) + + describe('user with priority compile', () => + beforeEach(function() { + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith(1, null, { compileGroup: 'priority' }) + return this.CompileController.proxyToClsi( + this.project_id, + (this.url = '/test'), + this.req, + this.res, + this.next + ) + })) + + describe('user with standard priority via query string', function() { + beforeEach(function() { + this.req.query = { compileGroup: 'standard' } + return this.CompileController.proxyToClsi( + this.project_id, + (this.url = '/test'), + this.req, + this.res, + this.next + ) + }) + + it('should open a request to the CLSI', function() { + return this.request + .calledWith({ + jar: this.jar, + method: this.req.method, + url: `${this.settings.apis.clsi.url}${this.url}`, + timeout: 60 * 1000 + }) + .should.equal(true) + }) + + it('should pass the request on to the client', function() { + return this.proxy.pipe.calledWith(this.res).should.equal(true) + }) + + return it('should bind an error handle to the request proxy', function() { + return this.proxy.on.calledWith('error').should.equal(true) + }) + }) + + describe('user with non-existent priority via query string', function() { + beforeEach(function() { + this.req.query = { compileGroup: 'foobar' } + return this.CompileController.proxyToClsi( + this.project_id, + (this.url = '/test'), + this.req, + this.res, + this.next + ) + }) + + return it('should proxy to the standard url', function() { + return this.request + .calledWith({ + jar: this.jar, + method: this.req.method, + url: `${this.settings.apis.clsi.url}${this.url}`, + timeout: 60 * 1000 + }) + .should.equal(true) + }) + }) + + return describe('user with build parameter via query string', function() { + beforeEach(function() { + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith(1, null, { compileGroup: 'standard' }) + this.req.query = { build: 1234 } + return this.CompileController.proxyToClsi( + this.project_id, + (this.url = '/test'), + this.req, + this.res, + this.next + ) + }) + + return it('should proxy to the standard url without the build parameter', function() { + return this.request + .calledWith({ + jar: this.jar, + method: this.req.method, + url: `${this.settings.apis.clsi.url}${this.url}`, + timeout: 60 * 1000 + }) + .should.equal(true) + }) + }) + }) + + return describe('new pdf viewer', function() { + beforeEach(function() { + return (this.req.query = { pdfng: true }) + }) + describe('user with standard priority', function() { + beforeEach(function() { + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith(1, null, { compileGroup: 'standard' }) + return this.CompileController.proxyToClsi( + this.project_id, + (this.url = '/test'), + this.req, + this.res, + this.next + ) + }) + + it('should open a request to the CLSI', function() { + return this.request + .calledWith({ + jar: this.jar, + method: this.req.method, + url: `${this.settings.apis.clsi.url}${this.url}`, + timeout: 60 * 1000, + headers: { + Range: '123-456', + 'If-Range': 'abcdef', + 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT' + } + }) + .should.equal(true) + }) + + it('should pass the request on to the client', function() { + return this.proxy.pipe.calledWith(this.res).should.equal(true) + }) + + return it('should bind an error handle to the request proxy', function() { + return this.proxy.on.calledWith('error').should.equal(true) + }) + }) + + return describe('user with build parameter via query string', function() { + beforeEach(function() { + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith(1, null, { compileGroup: 'standard' }) + this.req.query = { build: 1234, pdfng: true } + return this.CompileController.proxyToClsi( + this.project_id, + (this.url = '/test'), + this.req, + this.res, + this.next + ) + }) + + return it('should proxy to the standard url with the build parameter', function() { + return this.request + .calledWith({ + jar: this.jar, + method: this.req.method, + qs: { build: 1234 }, + url: `${this.settings.apis.clsi.url}${this.url}`, + timeout: 60 * 1000, + headers: { + Range: '123-456', + 'If-Range': 'abcdef', + 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT' + } + }) + .should.equal(true) + }) + }) + }) + }) + + describe('deleteAuxFiles', function() { + beforeEach(function() { + this.CompileManager.deleteAuxFiles = sinon.stub().callsArg(2) + this.req.params = { Project_id: this.project_id } + this.res.sendStatus = sinon.stub() + return this.CompileController.deleteAuxFiles( + this.req, + this.res, + this.next + ) + }) + + it('should proxy to the CLSI', function() { + return this.CompileManager.deleteAuxFiles + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should return a 200', function() { + return this.res.sendStatus.calledWith(200).should.equal(true) + }) + }) + + describe('compileAndDownloadPdf', function() { + beforeEach(function() { + this.req = { + params: { + project_id: this.project_id + } + } + this.CompileManager.compile.callsArgWith(3) + this.CompileController.proxyToClsi = sinon.stub() + return (this.res = { send: () => {} }) + }) + + it('should call compile in the compile manager', function(done) { + this.CompileController.compileAndDownloadPdf(this.req, this.res) + this.CompileManager.compile.calledWith(this.project_id).should.equal(true) + return done() + }) + + return it('should proxy the res to the clsi with correct url', function(done) { + this.CompileController.compileAndDownloadPdf(this.req, this.res) + sinon.assert.calledWith( + this.CompileController.proxyToClsi, + this.project_id, + `/project/${this.project_id}/output/output.pdf`, + this.req, + this.res + ) + + this.CompileController.proxyToClsi + .calledWith( + this.project_id, + `/project/${this.project_id}/output/output.pdf`, + this.req, + this.res + ) + .should.equal(true) + return done() + }) + }) + + return describe('wordCount', function() { + beforeEach(function() { + this.CompileManager.wordCount = sinon + .stub() + .callsArgWith(3, null, { content: 'body' }) + this.req.params = { Project_id: this.project_id } + this.res.send = sinon.stub() + this.res.contentType = sinon.stub() + return this.CompileController.wordCount(this.req, this.res, this.next) + }) + + it('should proxy to the CLSI', function() { + return this.CompileManager.wordCount + .calledWith(this.project_id, this.user_id, false) + .should.equal(true) + }) + + return it('should return a 200 and body', function() { + return this.res.send.calledWith({ content: 'body' }).should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Compile/CompileManagerTests.js b/services/web/test/unit/src/Compile/CompileManagerTests.js new file mode 100644 index 0000000000..2d95d49055 --- /dev/null +++ b/services/web/test/unit/src/Compile/CompileManagerTests.js @@ -0,0 +1,414 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Compile/CompileManager.js' +const { assert } = require('chai') +const SandboxedModule = require('sandboxed-module') + +describe('CompileManager', function() { + beforeEach(function() { + let Timer + this.rateLimitGetStub = sinon.stub() + const { rateLimitGetStub } = this + this.ratelimiter = { addCount: sinon.stub() } + this.CompileManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = { + redis: { web: { host: 'localhost', port: 42 } } + }), + '../../infrastructure/RedisWrapper': { + client: () => { + return (this.rclient = { auth() {} }) + } + }, + '../Project/ProjectRootDocManager': (this.ProjectRootDocManager = {}), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../User/UserGetter': (this.UserGetter = {}), + './ClsiManager': (this.ClsiManager = {}), + '../../infrastructure/RateLimiter': this.ratelimiter, + 'metrics-sharelatex': (this.Metrics = { + Timer: (Timer = (function() { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()), + inc: sinon.stub() + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + warn: sinon.stub() + }) + } + }) + this.project_id = 'mock-project-id-123' + this.user_id = 'mock-user-id-123' + this.callback = sinon.stub() + return (this.limits = { + timeout: 42 + }) + }) + + describe('compile', function() { + beforeEach(function() { + this.CompileManager._checkIfRecentlyCompiled = sinon + .stub() + .callsArgWith(2, null, false) + this.ProjectRootDocManager.ensureRootDocumentIsSet = sinon + .stub() + .callsArgWith(1, null) + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith(1, null, this.limits) + return (this.ClsiManager.sendRequest = sinon + .stub() + .callsArgWith( + 3, + null, + (this.status = 'mock-status'), + (this.outputFiles = 'mock output files'), + (this.output = 'mock output') + )) + }) + + describe('succesfully', function() { + beforeEach(function() { + this.CompileManager._checkIfAutoCompileLimitHasBeenHit = ( + isAutoCompile, + compileGroup, + cb + ) => cb(null, true) + return this.CompileManager.compile( + this.project_id, + this.user_id, + {}, + this.callback + ) + }) + + it('should check the project has not been recently compiled', function() { + return this.CompileManager._checkIfRecentlyCompiled + .calledWith(this.project_id, this.user_id) + .should.equal(true) + }) + + it('should ensure that the root document is set', function() { + return this.ProjectRootDocManager.ensureRootDocumentIsSet + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should get the project compile limits', function() { + return this.CompileManager.getProjectCompileLimits + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should run the compile with the compile limits', function() { + return this.ClsiManager.sendRequest + .calledWith(this.project_id, this.user_id, { + timeout: this.limits.timeout + }) + .should.equal(true) + }) + + it('should call the callback with the output', function() { + return this.callback + .calledWith(null, this.status, this.outputFiles, this.output) + .should.equal(true) + }) + + it('should time the compile', function() { + return this.Metrics.Timer.prototype.done.called.should.equal(true) + }) + + return it('should log out the compile', function() { + return this.logger.log + .calledWith( + { project_id: this.project_id, user_id: this.user_id }, + 'compiling project' + ) + .should.equal(true) + }) + }) + + describe('when the project has been recently compiled', () => + it('should return', function(done) { + this.CompileManager._checkIfAutoCompileLimitHasBeenHit = ( + isAutoCompile, + compileGroup, + cb + ) => cb(null, true) + this.CompileManager._checkIfRecentlyCompiled = sinon + .stub() + .callsArgWith(2, null, true) + return this.CompileManager.compile( + this.project_id, + this.user_id, + {}, + function(err, status) { + status.should.equal('too-recently-compiled') + return done() + } + ) + })) + + return describe('should check the rate limit', () => + it('should return', function(done) { + this.CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon + .stub() + .callsArgWith(2, null, false) + return this.CompileManager.compile( + this.project_id, + this.user_id, + {}, + function(err, status) { + status.should.equal('autocompile-backoff') + return done() + } + ) + })) + }) + + describe('getProjectCompileLimits', function() { + beforeEach(function() { + this.features = { + compileTimeout: (this.timeout = 42), + compileGroup: (this.group = 'priority') + } + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith( + 2, + null, + (this.project = { owner_ref: (this.owner_id = 'owner-id-123') }) + ) + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, (this.user = { features: this.features })) + return this.CompileManager.getProjectCompileLimits( + this.project_id, + this.callback + ) + }) + + it('should look up the owner of the project', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { owner_ref: 1 }) + .should.equal(true) + }) + + it("should look up the owner's features", function() { + return this.UserGetter.getUser + .calledWith(this.project.owner_ref, { features: 1 }) + .should.equal(true) + }) + + return it('should return the limits', function() { + return this.callback + .calledWith(null, { + timeout: this.timeout, + compileGroup: this.group + }) + .should.equal(true) + }) + }) + + describe('deleteAuxFiles', function() { + beforeEach(function() { + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith( + 1, + null, + (this.limits = { compileGroup: 'mock-compile-group' }) + ) + this.ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3) + return this.CompileManager.deleteAuxFiles( + this.project_id, + this.user_id, + this.callback + ) + }) + + it('should look up the compile group to use', function() { + return this.CompileManager.getProjectCompileLimits + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should delete the aux files', function() { + return this.ClsiManager.deleteAuxFiles + .calledWith(this.project_id, this.user_id, this.limits) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('_checkIfRecentlyCompiled', function() { + describe('when the key exists in redis', function() { + beforeEach(function() { + this.rclient.set = sinon.stub().callsArgWith(5, null, null) + return this.CompileManager._checkIfRecentlyCompiled( + this.project_id, + this.user_id, + this.callback + ) + }) + + it('should try to set the key', function() { + return this.rclient.set + .calledWith( + `compile:${this.project_id}:${this.user_id}`, + true, + 'EX', + this.CompileManager.COMPILE_DELAY, + 'NX' + ) + .should.equal(true) + }) + + return it('should call the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + return describe('when the key does not exist in redis', function() { + beforeEach(function() { + this.rclient.set = sinon.stub().callsArgWith(5, null, 'OK') + return this.CompileManager._checkIfRecentlyCompiled( + this.project_id, + this.user_id, + this.callback + ) + }) + + it('should try to set the key', function() { + return this.rclient.set + .calledWith( + `compile:${this.project_id}:${this.user_id}`, + true, + 'EX', + this.CompileManager.COMPILE_DELAY, + 'NX' + ) + .should.equal(true) + }) + + return it('should call the callback with false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + }) + + describe('_checkIfAutoCompileLimitHasBeenHit', function() { + it('should be able to compile if it is not an autocompile', function(done) { + this.ratelimiter.addCount.callsArgWith(2, null, true) + return this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + false, + 'everyone', + (err, canCompile) => { + canCompile.should.equal(true) + return done() + } + ) + }) + + it('should be able to compile if rate limit has remianing', function(done) { + this.ratelimiter.addCount.callsArgWith(1, null, true) + return this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + true, + 'everyone', + (err, canCompile) => { + const args = this.ratelimiter.addCount.args[0][0] + args.throttle.should.equal(25) + args.subjectName.should.equal('everyone') + args.timeInterval.should.equal(20) + args.endpointName.should.equal('auto_compile') + canCompile.should.equal(true) + return done() + } + ) + }) + + it('should be not able to compile if rate limit has no remianing', function(done) { + this.ratelimiter.addCount.callsArgWith(1, null, false) + return this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + true, + 'everyone', + (err, canCompile) => { + canCompile.should.equal(false) + return done() + } + ) + }) + + return it('should return false if there is an error in the rate limit', function(done) { + this.ratelimiter.addCount.callsArgWith(1, 'error') + return this.CompileManager._checkIfAutoCompileLimitHasBeenHit( + true, + 'everyone', + (err, canCompile) => { + canCompile.should.equal(false) + return done() + } + ) + }) + }) + + return describe('wordCount', function() { + beforeEach(function() { + this.CompileManager.getProjectCompileLimits = sinon + .stub() + .callsArgWith( + 1, + null, + (this.limits = { compileGroup: 'mock-compile-group' }) + ) + this.ClsiManager.wordCount = sinon.stub().callsArg(4) + return this.CompileManager.wordCount( + this.project_id, + this.user_id, + false, + this.callback + ) + }) + + it('should look up the compile group to use', function() { + return this.CompileManager.getProjectCompileLimits + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should call wordCount for project', function() { + return this.ClsiManager.wordCount + .calledWith(this.project_id, this.user_id, false, this.limits) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Contact/ContactControllerTests.js b/services/web/test/unit/src/Contact/ContactControllerTests.js new file mode 100644 index 0000000000..af5cc3db5d --- /dev/null +++ b/services/web/test/unit/src/Contact/ContactControllerTests.js @@ -0,0 +1,137 @@ +/* eslint-disable + max-len, + no-dupe-keys, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { assert } = chai +const { expect } = chai +const modulePath = '../../../../app/src/Features/Contacts/ContactController.js' +const SandboxedModule = require('sandboxed-module') + +describe('ContactController', function() { + beforeEach(function() { + this.AuthenticationController = { getLoggedInUserId: sinon.stub() } + this.ContactController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + '../User/UserGetter': (this.UserGetter = {}), + './ContactManager': (this.ContactManager = {}), + '../Authentication/AuthenticationController': (this.AuthenticationController = {}), + '../../infrastructure/Modules': (this.Modules = { hooks: {} }), + '../Authentication/AuthenticationController': this + .AuthenticationController + } + }) + + this.next = sinon.stub() + this.req = {} + this.res = {} + this.res.status = sinon.stub().returns(this.req) + return (this.res.send = sinon.stub()) + }) + + return describe('getContacts', function() { + beforeEach(function() { + this.user_id = 'mock-user-id' + this.contact_ids = ['contact-1', 'contact-2', 'contact-3'] + this.contacts = [ + { + _id: 'contact-1', + email: 'joe@example.com', + first_name: 'Joe', + last_name: 'Example', + unsued: 'foo' + }, + { + _id: 'contact-2', + email: 'jane@example.com', + first_name: 'Jane', + last_name: 'Example', + unsued: 'foo', + holdingAccount: true + }, + { + _id: 'contact-3', + email: 'jim@example.com', + first_name: 'Jim', + last_name: 'Example', + unsued: 'foo' + } + ] + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.user_id) + this.ContactManager.getContactIds = sinon + .stub() + .callsArgWith(2, null, this.contact_ids) + this.UserGetter.getUsers = sinon + .stub() + .callsArgWith(2, null, this.contacts) + this.Modules.hooks.fire = sinon.stub().callsArg(3) + + return this.ContactController.getContacts(this.req, this.res, this.next) + }) + + it('should look up the logged in user id', function() { + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should get the users contact ids', function() { + return this.ContactManager.getContactIds + .calledWith(this.user_id, { limit: 50 }) + .should.equal(true) + }) + + it('should populate the users contacts ids', function() { + return this.UserGetter.getUsers + .calledWith(this.contact_ids, { + email: 1, + first_name: 1, + last_name: 1, + holdingAccount: 1 + }) + .should.equal(true) + }) + + it('should fire the getContact module hook', function() { + return this.Modules.hooks.fire + .calledWith('getContacts', this.user_id) + .should.equal(true) + }) + + return it('should return a formatted list of contacts in contact list order, without holding accounts', function() { + return this.res.send.args[0][0].contacts.should.deep.equal([ + { + id: 'contact-1', + email: 'joe@example.com', + first_name: 'Joe', + last_name: 'Example', + type: 'user' + }, + { + id: 'contact-3', + email: 'jim@example.com', + first_name: 'Jim', + last_name: 'Example', + type: 'user' + } + ]) + }) + }) +}) diff --git a/services/web/test/unit/src/Contact/ContactManagerTests.js b/services/web/test/unit/src/Contact/ContactManagerTests.js new file mode 100644 index 0000000000..da6e7701ef --- /dev/null +++ b/services/web/test/unit/src/Contact/ContactManagerTests.js @@ -0,0 +1,185 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +chai.should() +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Contacts/ContactManager' +const SandboxedModule = require('sandboxed-module') + +describe('ContactManager', function() { + beforeEach(function() { + this.ContactManager = SandboxedModule.require(modulePath, { + requires: { + request: (this.request = sinon.stub()), + 'settings-sharelatex': (this.settings = { + apis: { + contacts: { + url: 'contacts.sharelatex.com' + } + } + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }) + } + }) + + this.user_id = 'user-id-123' + this.contact_id = 'contact-id-123' + return (this.callback = sinon.stub()) + }) + + describe('getContacts', function() { + describe('with a successful response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + null, + { statusCode: 204 }, + { contact_ids: (this.contact_ids = ['mock', 'contact_ids']) } + ) + return this.ContactManager.getContactIds( + this.user_id, + { limit: 42 }, + this.callback + ) + }) + + it('should get the contacts from the contacts api', function() { + return this.request.get + .calledWith({ + url: `${this.settings.apis.contacts.url}/user/${ + this.user_id + }/contacts`, + qs: { limit: 42 }, + json: true, + jar: false + }) + .should.equal(true) + }) + + return it('should call the callback with the contatcs', function() { + return this.callback + .calledWith(null, this.contact_ids) + .should.equal(true) + }) + }) + + return describe('with a failed response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, null) + return this.ContactManager.getContactIds( + this.user_id, + { limit: 42 }, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('contacts api responded with non-success code: 500') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Error( + 'contacts api responded with a non-success code: 500' + ), + user_id: this.user_id + }, + 'error getting contacts for user' + ) + .should.equal(true) + }) + }) + }) + + return describe('addContact', function() { + describe('with a successful response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, null) + return this.ContactManager.addContact( + this.user_id, + this.contact_id, + this.callback + ) + }) + + it('should add the contacts for the user in the contacts api', function() { + return this.request.post + .calledWith({ + url: `${this.settings.apis.contacts.url}/user/${ + this.user_id + }/contacts`, + json: { + contact_id: this.contact_id + }, + jar: false + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('with a failed response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, null) + return this.ContactManager.addContact( + this.user_id, + this.contact_id, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('contacts api responded with non-success code: 500') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Error( + 'contacts api responded with a non-success code: 500' + ), + user_id: this.user_id, + contact_id: this.contact_id + }, + 'error adding contact for user' + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Cooldown/CooldownManagerTests.js b/services/web/test/unit/src/Cooldown/CooldownManagerTests.js new file mode 100644 index 0000000000..6abf2102bd --- /dev/null +++ b/services/web/test/unit/src/Cooldown/CooldownManagerTests.js @@ -0,0 +1,182 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const { expect } = require('chai') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Cooldown/CooldownManager' +) + +describe('CooldownManager', function() { + beforeEach(function() { + this.projectId = 'abcdefg' + this.rclient = { set: sinon.stub(), get: sinon.stub() } + this.RedisWrapper = { client: () => this.rclient } + return (this.CooldownManager = SandboxedModule.require(modulePath, { + requires: { + '../../infrastructure/RedisWrapper': this.RedisWrapper, + 'logger-sharelatex': { log: sinon.stub() } + } + })) + }) + + describe('_buildKey', () => + it('should build a properly formatted redis key', function() { + return expect(this.CooldownManager._buildKey('ABC')).to.equal( + 'Cooldown:{ABC}' + ) + })) + + describe('isProjectOnCooldown', function() { + beforeEach(function() { + return (this.call = cb => { + return this.CooldownManager.isProjectOnCooldown(this.projectId, cb) + }) + }) + + describe('when project is on cooldown', function() { + beforeEach(function() { + return (this.rclient.get = sinon.stub().callsArgWith(1, null, '1')) + }) + + it('should fetch key from redis', function(done) { + return this.call((err, result) => { + this.rclient.get.callCount.should.equal(1) + this.rclient.get.calledWith('Cooldown:{abcdefg}').should.equal(true) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.equal(null) + return done() + }) + }) + + return it('should produce a true result', function(done) { + return this.call((err, result) => { + expect(result).to.equal(true) + return done() + }) + }) + }) + + describe('when project is not on cooldown', function() { + beforeEach(function() { + return (this.rclient.get = sinon.stub().callsArgWith(1, null, null)) + }) + + it('should fetch key from redis', function(done) { + return this.call((err, result) => { + this.rclient.get.callCount.should.equal(1) + this.rclient.get.calledWith('Cooldown:{abcdefg}').should.equal(true) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.equal(null) + return done() + }) + }) + + return it('should produce a false result', function(done) { + return this.call((err, result) => { + expect(result).to.equal(false) + return done() + }) + }) + }) + + return describe('when rclient.get produces an error', function() { + beforeEach(function() { + return (this.rclient.get = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + it('should fetch key from redis', function(done) { + return this.call((err, result) => { + this.rclient.get.callCount.should.equal(1) + this.rclient.get.calledWith('Cooldown:{abcdefg}').should.equal(true) + return done() + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + return describe('putProjectOnCooldown', function() { + beforeEach(function() { + return (this.call = cb => { + return this.CooldownManager.putProjectOnCooldown(this.projectId, cb) + }) + }) + + describe('when rclient.set does not produce an error', function() { + beforeEach(function() { + return (this.rclient.set = sinon.stub().callsArgWith(4, null)) + }) + + it('should set a key in redis', function(done) { + return this.call(err => { + this.rclient.set.callCount.should.equal(1) + this.rclient.set.calledWith('Cooldown:{abcdefg}').should.equal(true) + return done() + }) + }) + + return it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.equal(null) + return done() + }) + }) + }) + + return describe('when rclient.set produces an error', function() { + beforeEach(function() { + return (this.rclient.set = sinon + .stub() + .callsArgWith(4, new Error('woops'))) + }) + + it('should set a key in redis', function(done) { + return this.call(err => { + this.rclient.set.callCount.should.equal(1) + this.rclient.set.calledWith('Cooldown:{abcdefg}').should.equal(true) + return done() + }) + }) + + return it('produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Cooldown/CooldownMiddlewareTests.js b/services/web/test/unit/src/Cooldown/CooldownMiddlewareTests.js new file mode 100644 index 0000000000..6f38a2f5ef --- /dev/null +++ b/services/web/test/unit/src/Cooldown/CooldownMiddlewareTests.js @@ -0,0 +1,137 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const { expect } = require('chai') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Cooldown/CooldownMiddleware' +) + +describe('CooldownMiddleware', function() { + beforeEach(function() { + this.CooldownManager = { isProjectOnCooldown: sinon.stub() } + return (this.CooldownMiddleware = SandboxedModule.require(modulePath, { + requires: { + './CooldownManager': this.CooldownManager, + 'logger-sharelatex': { log: sinon.stub() } + } + })) + }) + + return describe('freezeProject', function() { + describe('when project is on cooldown', function() { + beforeEach(function() { + this.CooldownManager.isProjectOnCooldown = sinon + .stub() + .callsArgWith(1, null, true) + this.req = { params: { Project_id: 'abc' } } + this.res = { sendStatus: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should call CooldownManager.isProjectOnCooldown', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return this.CooldownManager.isProjectOnCooldown + .calledWith('abc') + .should.equal(true) + }) + + it('should not produce an error', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + return it('should send a 429 status', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(429).should.equal(true) + }) + }) + + describe('when project is not on cooldown', function() { + beforeEach(function() { + this.CooldownManager.isProjectOnCooldown = sinon + .stub() + .callsArgWith(1, null, false) + this.req = { params: { Project_id: 'abc' } } + this.res = { sendStatus: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should call CooldownManager.isProjectOnCooldown', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return this.CooldownManager.isProjectOnCooldown + .calledWith('abc') + .should.equal(true) + }) + + return it('call next with no arguments', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args.length).to.equal(0) + }) + }) + + describe('when isProjectOnCooldown produces an error', function() { + beforeEach(function() { + this.CooldownManager.isProjectOnCooldown = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.req = { params: { Project_id: 'abc' } } + this.res = { sendStatus: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should call CooldownManager.isProjectOnCooldown', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return this.CooldownManager.isProjectOnCooldown + .calledWith('abc') + .should.equal(true) + }) + + return it('call next with an error', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + }) + + return describe('when projectId is not part of route', function() { + beforeEach(function() { + this.CooldownManager.isProjectOnCooldown = sinon + .stub() + .callsArgWith(1, null, true) + this.req = { params: { lol: 'abc' } } + this.res = { sendStatus: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('call next with an error', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + return it('should not call CooldownManager.isProjectOnCooldown', function() { + this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) + return this.CooldownManager.isProjectOnCooldown.callCount.should.equal( + 0 + ) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Docstore/DocstoreManagerTests.js b/services/web/test/unit/src/Docstore/DocstoreManagerTests.js new file mode 100644 index 0000000000..385e2f300e --- /dev/null +++ b/services/web/test/unit/src/Docstore/DocstoreManagerTests.js @@ -0,0 +1,580 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +chai.should() +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Docstore/DocstoreManager' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/src/Features/Errors/Errors.js') + +describe('DocstoreManager', function() { + beforeEach(function() { + this.requestDefaults = sinon.stub().returns((this.request = sinon.stub())) + this.DocstoreManager = SandboxedModule.require(modulePath, { + requires: { + request: { + defaults: this.requestDefaults + }, + 'settings-sharelatex': (this.settings = { + apis: { + docstore: { + url: 'docstore.sharelatex.com' + } + } + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }) + } + }) + + this.requestDefaults.calledWith({ jar: false }).should.equal(true) + + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-123' + return (this.callback = sinon.stub()) + }) + + describe('deleteDoc', function() { + describe('with a successful response code', function() { + beforeEach(function() { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.DocstoreManager.deleteDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should delete the doc in the docstore api', function() { + return this.request.del + .calledWith( + `${this.settings.apis.docstore.url}/project/${ + this.project_id + }/doc/${this.doc_id}` + ) + .should.equal(true) + }) + + return it('should call the callback without an error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('with a failed response code', function() { + beforeEach(function() { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocstoreManager.deleteDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('docstore api responded with non-success code: 500') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Error( + 'docstore api responded with a non-success code: 500' + ), + project_id: this.project_id, + doc_id: this.doc_id + }, + 'error deleting doc in docstore' + ) + .should.equal(true) + }) + }) + + return describe('with a missing (404) response code', function() { + beforeEach(function() { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 404 }, '') + return this.DocstoreManager.deleteDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Errors.NotFoundError('tried to delete doc not in docstore') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Errors.NotFoundError( + 'tried to delete doc not in docstore' + ), + project_id: this.project_id, + doc_id: this.doc_id + }, + 'tried to delete doc not in docstore' + ) + .should.equal(true) + }) + }) + }) + + describe('updateDoc', function() { + beforeEach(function() { + this.lines = ['mock', 'doc', 'lines'] + this.rev = 5 + this.version = 42 + this.ranges = { mock: 'ranges' } + return (this.modified = true) + }) + + describe('with a successful response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith( + 1, + null, + { statusCode: 204 }, + { modified: this.modified, rev: this.rev } + ) + return this.DocstoreManager.updateDoc( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.callback + ) + }) + + it('should update the doc in the docstore api', function() { + return this.request.post + .calledWith({ + url: `${this.settings.apis.docstore.url}/project/${ + this.project_id + }/doc/${this.doc_id}`, + json: { + lines: this.lines, + version: this.version, + ranges: this.ranges + } + }) + .should.equal(true) + }) + + return it('should call the callback with the modified status and revision', function() { + return this.callback + .calledWith(null, this.modified, this.rev) + .should.equal(true) + }) + }) + + return describe('with a failed response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocstoreManager.updateDoc( + this.project_id, + this.doc_id, + this.lines, + this.version, + this.ranges, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('docstore api responded with non-success code: 500') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Error( + 'docstore api responded with a non-success code: 500' + ), + project_id: this.project_id, + doc_id: this.doc_id + }, + 'error updating doc in docstore' + ) + .should.equal(true) + }) + }) + }) + + describe('getDoc', function() { + beforeEach(function() { + return (this.doc = { + lines: (this.lines = ['mock', 'doc', 'lines']), + rev: (this.rev = 5), + version: (this.version = 42), + ranges: (this.ranges = { mock: 'ranges' }) + }) + }) + + describe('with a successful response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, this.doc) + return this.DocstoreManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should get the doc from the docstore api', function() { + return this.request.get + .calledWith({ + url: `${this.settings.apis.docstore.url}/project/${ + this.project_id + }/doc/${this.doc_id}`, + json: true + }) + .should.equal(true) + }) + + return it('should call the callback with the lines, version and rev', function() { + return this.callback + .calledWith(null, this.lines, this.rev, this.version, this.ranges) + .should.equal(true) + }) + }) + + describe('with a failed response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocstoreManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('docstore api responded with non-success code: 500') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Error( + 'docstore api responded with a non-success code: 500' + ), + project_id: this.project_id, + doc_id: this.doc_id + }, + 'error getting doc from docstore' + ) + .should.equal(true) + }) + }) + + describe('with include_deleted=true', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, this.doc) + return this.DocstoreManager.getDoc( + this.project_id, + this.doc_id, + { include_deleted: true }, + this.callback + ) + }) + + it('should get the doc from the docstore api (including deleted)', function() { + return this.request.get + .calledWith({ + url: `${this.settings.apis.docstore.url}/project/${ + this.project_id + }/doc/${this.doc_id}?include_deleted=true`, + json: true + }) + .should.equal(true) + }) + + return it('should call the callback with the lines, version and rev', function() { + return this.callback + .calledWith(null, this.lines, this.rev, this.version, this.ranges) + .should.equal(true) + }) + }) + + return describe('with a missing (404) response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 404 }, '') + return this.DocstoreManager.getDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith(new Errors.NotFoundError('doc not found in docstore')) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Errors.NotFoundError('doc not found in docstore'), + project_id: this.project_id, + doc_id: this.doc_id + }, + 'doc not found in docstore' + ) + .should.equal(true) + }) + }) + }) + + describe('getAllDocs', function() { + describe('with a successful response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + null, + { statusCode: 204 }, + (this.docs = [{ _id: 'mock-doc-id' }]) + ) + return this.DocstoreManager.getAllDocs(this.project_id, this.callback) + }) + + it('should get all the project docs in the docstore api', function() { + return this.request.get + .calledWith({ + url: `${this.settings.apis.docstore.url}/project/${ + this.project_id + }/doc`, + json: true + }) + .should.equal(true) + }) + + return it('should call the callback with the docs', function() { + return this.callback.calledWith(null, this.docs).should.equal(true) + }) + }) + + return describe('with a failed response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocstoreManager.getAllDocs(this.project_id, this.callback) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('docstore api responded with non-success code: 500') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Error( + 'docstore api responded with a non-success code: 500' + ), + project_id: this.project_id + }, + 'error getting all docs from docstore' + ) + .should.equal(true) + }) + }) + }) + + describe('getAllRanges', function() { + describe('with a successful response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + null, + { statusCode: 204 }, + (this.docs = [{ _id: 'mock-doc-id', ranges: 'mock-ranges' }]) + ) + return this.DocstoreManager.getAllRanges(this.project_id, this.callback) + }) + + it('should get all the project doc ranges in the docstore api', function() { + return this.request.get + .calledWith({ + url: `${this.settings.apis.docstore.url}/project/${ + this.project_id + }/ranges`, + json: true + }) + .should.equal(true) + }) + + return it('should call the callback with the docs', function() { + return this.callback.calledWith(null, this.docs).should.equal(true) + }) + }) + + return describe('with a failed response code', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocstoreManager.getAllRanges(this.project_id, this.callback) + }) + + it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('docstore api responded with non-success code: 500') + ) + .should.equal(true) + }) + + return it('should log the error', function() { + return this.logger.error + .calledWith( + { + err: new Error( + 'docstore api responded with a non-success code: 500' + ), + project_id: this.project_id + }, + 'error getting all doc ranges from docstore' + ) + .should.equal(true) + }) + }) + }) + + describe('archiveProject', function() { + describe('with a successful response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }) + return this.DocstoreManager.archiveProject( + this.project_id, + this.callback + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('with a failed response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }) + return this.DocstoreManager.archiveProject( + this.project_id, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('docstore api responded with non-success code: 500') + ) + .should.equal(true) + }) + }) + }) + + return describe('unarchiveProject', function() { + describe('with a successful response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }) + return this.DocstoreManager.unarchiveProject( + this.project_id, + this.callback + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('with a failed response code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }) + return this.DocstoreManager.unarchiveProject( + this.project_id, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWith( + new Error('docstore api responded with non-success code: 500') + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js new file mode 100644 index 0000000000..75588a82c9 --- /dev/null +++ b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterHandlerTests.js @@ -0,0 +1,1034 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const spies = require('chai-spies') +const chai = require('chai').use(spies) +const sinon = require('sinon') +const SandboxedModule = require('sandboxed-module') +const { assert } = require('chai') +const path = require('path') +const _ = require('underscore') +const { ObjectId } = require('mongojs') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler' +) + +describe('DocumentUpdaterHandler', function() { + beforeEach(function() { + this.project_id = 'project-id-923' + this.projectHistoryId = 'ol-project-id-1' + this.doc_id = 'doc-id-394' + this.lines = ['one', 'two', 'three'] + this.version = 42 + this.user_id = 'mock-user-id-123' + this.project = { _id: this.project_id } + + this.request = sinon.stub() + this.projectEntityHandler = {} + this.settings = { + apis: { + documentupdater: { + url: 'http://document_updater.example.com' + }, + project_history: { + url: 'http://project_history.example.com' + } + } + } + + this.callback = sinon.stub() + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + request: { + defaults: () => { + return this.request + } + }, + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { log() {}, error() {}, warn() {} }, + '../Project/ProjectEntityHandler': this.projectEntityHandler, + '../../models/Project': { + Project: (this.Project = {}) + }, + '../../Features/Project/ProjectLocator': {}, + 'metrics-sharelatex': { + Timer: class { + done() {} + } + } + } + })) + }) + + describe('flushProjectToMongo', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.flushProjectToMongo(this.project_id, this.callback) + }) + + it('should flush the document from the document updater', function() { + return this.request + .calledWithMatch({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/flush`, + method: 'POST' + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.flushProjectToMongo(this.project_id, this.callback) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.flushProjectToMongo(this.project_id, this.callback) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('flushProjectToMongoAndDelete', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) + + it('should delete the project from the document updater', function() { + return this.request + .calledWithMatch({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }`, + method: 'DELETE' + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('flushDocToMongo', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.flushDocToMongo( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should flush the document from the document updater', function() { + return this.request + .calledWithMatch({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/doc/${this.doc_id}/flush`, + method: 'POST' + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.flushDocToMongo( + this.project_id, + this.doc_id, + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.flushDocToMongo( + this.project_id, + this.doc_id, + this.callback + ) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('deleteDoc', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.deleteDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + it('should delete the document from the document updater', function() { + return this.request + .calledWithMatch({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/doc/${this.doc_id}`, + method: 'DELETE' + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.deleteDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.deleteDoc( + this.project_id, + this.doc_id, + this.callback + ) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('setDocument', function() { + beforeEach(function() { + return (this.source = 'dropbox') + }) + + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.setDocument( + this.project_id, + this.doc_id, + this.user_id, + this.lines, + this.source, + this.callback + ) + }) + + it('should set the document in the document updater', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/doc/${this.doc_id}`, + json: { + lines: this.lines, + source: this.source, + user_id: this.user_id + }, + method: 'POST' + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.setDocument( + this.project_id, + this.doc_id, + this.user_id, + this.lines, + this.source, + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.setDocument( + this.project_id, + this.doc_id, + this.user_id, + this.lines, + this.source, + this.callback + ) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('getDocument', function() { + describe('successfully', function() { + beforeEach(function() { + this.body = { + lines: this.lines, + version: this.version, + ops: (this.ops = ['mock-op-1', 'mock-op-2']), + ranges: (this.ranges = { mock: 'ranges' }) + } + this.fromVersion = 2 + this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.handler.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) + + it('should get the document from the document updater', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/doc/${this.doc_id}?fromVersion=${this.fromVersion}`, + method: 'GET', + json: true + }) + .should.equal(true) + }) + + return it('should call the callback with the lines and version', function() { + return this.callback + .calledWith(null, this.lines, this.version, this.ranges, this.ops) + .should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('getProjectDocsIfMatch', function() { + beforeEach(function() { + return (this.project_state_hash = '1234567890abcdef') + }) + + describe('successfully', function() { + beforeEach(function() { + this.doc0 = { + _id: this.doc_id, + lines: this.lines, + v: this.version + } + this.docs = [this.doc0, this.doc0, this.doc0] + this.body = JSON.stringify(this.docs) + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.handler.getProjectDocsIfMatch( + this.project_id, + this.project_state_hash, + this.callback + ) + }) + + it('should get the documents from the document updater', function() { + const url = `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/get_and_flush_if_old?state=${this.project_state_hash}` + return this.request.post.calledWith(url).should.equal(true) + }) + + return it('should call the callback with the documents', function() { + return this.callback + .calledWithExactly(null, this.docs) + .should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.getProjectDocsIfMatch( + this.project_id, + this.project_state_hash, + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a conflict error code', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 409 }, 'Conflict') + return this.handler.getProjectDocsIfMatch( + this.project_id, + this.project_state_hash, + this.callback + ) + }) + + return it('should return the callback with no documents', function() { + return this.callback.alwaysCalledWithExactly().should.equal(true) + }) + }) + }) + + describe('clearProjectState', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 200 }) + return this.handler.clearProjectState(this.project_id, this.callback) + }) + + it('should clear the project state from the document updater', function() { + return this.request + .calledWithMatch({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/clearState`, + method: 'POST' + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.clearProjectState(this.project_id, this.callback) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns an error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, null) + return this.handler.clearProjectState(this.project_id, this.callback) + }) + + return it('should return the callback with no documents', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('acceptChanges', function() { + beforeEach(function() { + return (this.change_id = 'mock-change-id-1') + }) + + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.handler.acceptChanges( + this.project_id, + this.doc_id, + [this.change_id], + this.callback + ) + }) + + it('should accept the change in the document updater', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/doc/${this.doc_id}/change/accept`, + json: { + change_ids: [this.change_id] + }, + method: 'POST' + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.acceptChanges( + this.project_id, + this.doc_id, + [this.change_id], + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.acceptChanges( + this.project_id, + this.doc_id, + [this.change_id], + this.callback + ) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + describe('deleteThread', function() { + beforeEach(function() { + return (this.thread_id = 'mock-thread-id-1') + }) + + describe('successfully', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.handler.deleteThread( + this.project_id, + this.doc_id, + this.thread_id, + this.callback + ) + }) + + it('should delete the thread in the document updater', function() { + return this.request + .calledWithMatch({ + url: `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }/doc/${this.doc_id}/comment/${this.thread_id}`, + method: 'DELETE' + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the document updater API returns an error', function() { + beforeEach(function() { + this.request.callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.handler.deleteThread( + this.project_id, + this.doc_id, + this.thread_id, + this.callback + ) + }) + + return it('should return an error to the callback', function() { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('when the document updater returns a failure error code', function() { + beforeEach(function() { + this.request.callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.deleteThread( + this.project_id, + this.doc_id, + this.thread_id, + this.callback + ) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + new Error('doc updater returned failure status code: 500') + ) + .should.equal(true) + }) + }) + }) + + return describe('updateProjectStructure ', function() { + beforeEach(function() { + this.user_id = 1234 + return (this.version = 999) + }) + + describe('with project history disabled', function() { + beforeEach(function() { + this.settings.apis.project_history.sendProjectStructureOps = false + return this.handler.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + {}, + this.callback + ) + }) + + it('does not make a web request', function() { + return this.request.called.should.equal(false) + }) + + return it('calls the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('with project history enabled', function() { + beforeEach(function() { + this.settings.apis.project_history.sendProjectStructureOps = true + this.url = `${this.settings.apis.documentupdater.url}/project/${ + this.project_id + }` + return this.request.callsArgWith(1, null, { statusCode: 204 }, '') + }) + + describe('when an entity has changed name', () => + it('should send the structure update to the document updater', function(done) { + this.docIdA = new ObjectId() + this.docIdB = new ObjectId() + this.changes = { + oldDocs: [ + { path: '/old_a', doc: { _id: this.docIdA } }, + { path: '/old_b', doc: { _id: this.docIdB } } + ], + // create new instances of the same ObjectIds so that == doesn't pass + newDocs: [ + { + path: '/old_a', + doc: { _id: new ObjectId(this.docIdA.toString()) } + }, + { + path: '/new_b', + doc: { _id: new ObjectId(this.docIdB.toString()) } + } + ], + newProject: { version: this.version } + } + + const docUpdates = [ + { + id: this.docIdB.toString(), + pathname: '/old_b', + newPathname: '/new_b' + } + ] + + return this.handler.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + this.changes, + () => { + this.request + .calledWith({ + url: this.url, + method: 'POST', + json: { + docUpdates, + fileUpdates: [], + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId + } + }) + .should.equal(true) + return done() + } + ) + })) + + describe('when a doc has been added', () => + it('should send the structure update to the document updater', function(done) { + this.docId = new ObjectId() + this.changes = { + newDocs: [ + { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } } + ], + newProject: { version: this.version } + } + + const docUpdates = [ + { + id: this.docId.toString(), + pathname: '/foo', + docLines: 'a\nb', + url: undefined, + hash: undefined + } + ] + + return this.handler.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + this.changes, + () => { + this.request + .calledWith({ + url: this.url, + method: 'POST', + json: { + docUpdates, + fileUpdates: [], + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId + } + }) + .should.equal(true) + return done() + } + ) + })) + + describe('when a file has been added', () => + it('should send the structure update to the document updater', function(done) { + this.fileId = new ObjectId() + this.changes = { + newFiles: [ + { + path: '/bar', + url: 'filestore.example.com/file', + file: { _id: this.fileId, hash: '12345' } + } + ], + newProject: { version: this.version } + } + + const fileUpdates = [ + { + id: this.fileId.toString(), + pathname: '/bar', + url: 'filestore.example.com/file', + docLines: undefined, + hash: '12345' + } + ] + + return this.handler.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + this.changes, + () => { + this.request + .calledWith({ + url: this.url, + method: 'POST', + json: { + docUpdates: [], + fileUpdates, + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId + } + }) + .should.equal(true) + return done() + } + ) + })) + + describe('when an entity has been deleted', () => + it('should end the structure update to the document updater', function(done) { + this.docId = new ObjectId() + this.changes = { + oldDocs: [ + { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } } + ], + newProject: { version: this.version } + } + + const docUpdates = [ + { + id: this.docId.toString(), + pathname: '/foo', + newPathname: '' + } + ] + + return this.handler.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + this.changes, + () => { + this.request + .calledWith({ + url: this.url, + method: 'POST', + json: { + docUpdates, + fileUpdates: [], + userId: this.user_id, + version: this.version, + projectHistoryId: this.projectHistoryId + } + }) + .should.equal(true) + return done() + } + ) + })) + + return describe('when the project version is missing', () => + it('should call the callback with an error', function() { + this.docId = new ObjectId() + this.changes = { + oldDocs: [ + { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } } + ] + } + + const docUpdates = [ + { + id: this.docId.toString(), + pathname: '/foo', + newPathname: '' + } + ] + + this.handler.updateProjectStructure( + this.project_id, + this.projectHistoryId, + this.user_id, + this.changes, + this.callback + ) + + this.callback.calledWith(new Error()).should.equal(true) + const firstCallArgs = this.callback.args[0] + return firstCallArgs[0].message.should.equal( + 'did not receive project version in changes' + ) + })) + }) + }) +}) diff --git a/services/web/test/unit/src/Documents/DocumentControllerTests.js b/services/web/test/unit/src/Documents/DocumentControllerTests.js new file mode 100644 index 0000000000..bb0219e2a9 --- /dev/null +++ b/services/web/test/unit/src/Documents/DocumentControllerTests.js @@ -0,0 +1,271 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Documents/DocumentController.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('DocumentController', function() { + beforeEach(function() { + this.DocumentController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../Project/ProjectLocator': (this.ProjectLocator = {}), + '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), + '../Project/ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}) + } + }) + this.res = new MockResponse() + this.req = new MockRequest() + this.next = sinon.stub() + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-123' + this.doc_lines = ['one', 'two', 'three'] + this.version = 42 + this.ranges = { mock: 'ranges' } + this.pathname = '/a/b/c/file.tex' + this.lastUpdatedAt = new Date().getTime() + this.lastUpdatedBy = 'fake-last-updater-id' + return (this.rev = 5) + }) + + describe('getDocument', function() { + beforeEach(function() { + return (this.req.params = { + Project_id: this.project_id, + doc_id: this.doc_id + }) + }) + + describe('when the project exists without project history enabled', function() { + beforeEach(function() { + this.project = { _id: this.project_id } + return (this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project)) + }) + + describe('when the document exists', function() { + beforeEach(function() { + this.doc = { _id: this.doc_id } + this.ProjectLocator.findElement = sinon + .stub() + .callsArgWith(1, null, this.doc, { fileSystem: this.pathname }) + this.ProjectEntityHandler.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.doc_lines, + this.rev, + this.version, + this.ranges + ) + return this.DocumentController.getDocument( + this.req, + this.res, + this.next + ) + }) + + it('should get the project', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { rootFolder: true, overleaf: true }) + .should.equal(true) + }) + + it('should get the pathname of the document', function() { + return this.ProjectLocator.findElement + .calledWith({ + project: this.project, + element_id: this.doc_id, + type: 'doc' + }) + .should.equal(true) + }) + + it('should get the document content', function() { + return this.ProjectEntityHandler.getDoc + .calledWith(this.project_id, this.doc_id) + .should.equal(true) + }) + + return it('should return the document data to the client as JSON', function() { + this.res.type.should.equal('application/json') + return this.res.body.should.equal( + JSON.stringify({ + lines: this.doc_lines, + version: this.version, + ranges: this.ranges, + pathname: this.pathname + }) + ) + }) + }) + + return describe("when the document doesn't exist", function() { + beforeEach(function() { + this.ProjectLocator.findElement = sinon + .stub() + .callsArgWith(1, new Errors.NotFoundError('not found')) + return this.DocumentController.getDocument( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with the NotFoundError', function() { + return this.next + .calledWith(new Errors.NotFoundError('not found')) + .should.equal(true) + }) + }) + }) + + describe('when project exists with project history enabled', function() { + beforeEach(function() { + this.doc = { _id: this.doc_id } + this.projectHistoryId = 1234 + this.project = { + _id: this.project_id, + overleaf: { history: { id: this.projectHistoryId } } + } + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project) + this.ProjectLocator.findElement = sinon + .stub() + .callsArgWith(1, null, this.doc, { fileSystem: this.pathname }) + this.ProjectEntityHandler.getDoc = sinon + .stub() + .callsArgWith( + 2, + null, + this.doc_lines, + this.rev, + this.version, + this.ranges + ) + return this.DocumentController.getDocument( + this.req, + this.res, + this.next + ) + }) + + return it('should return the history id to the client as JSON', function() { + this.res.type.should.equal('application/json') + return this.res.body.should.equal( + JSON.stringify({ + lines: this.doc_lines, + version: this.version, + ranges: this.ranges, + pathname: this.pathname, + projectHistoryId: this.projectHistoryId + }) + ) + }) + }) + + return describe('when the project does not exist', function() { + beforeEach(function() { + this.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) + return this.DocumentController.getDocument( + this.req, + this.res, + this.next + ) + }) + + return it('returns a 404', function() { + return this.res.statusCode.should.equal(404) + }) + }) + }) + + return describe('setDocument', function() { + beforeEach(function() { + return (this.req.params = { + Project_id: this.project_id, + doc_id: this.doc_id + }) + }) + + describe('when the document exists', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.updateDocLines = sinon.stub().yields() + this.req.body = { + lines: this.doc_lines, + version: this.version, + ranges: this.ranges, + lastUpdatedAt: this.lastUpdatedAt, + lastUpdatedBy: this.lastUpdatedBy + } + return this.DocumentController.setDocument( + this.req, + this.res, + this.next + ) + }) + + it('should update the document in Mongo', function() { + return sinon.assert.calledWith( + this.ProjectEntityUpdateHandler.updateDocLines, + this.project_id, + this.doc_id, + this.doc_lines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy + ) + }) + + return it('should return a successful response', function() { + return this.res.success.should.equal(true) + }) + }) + + return describe("when the document doesn't exist", function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.updateDocLines = sinon + .stub() + .yields(new Errors.NotFoundError('document does not exist')) + this.req.body = { lines: this.doc_lines } + return this.DocumentController.setDocument( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with the NotFoundError', function() { + return this.next + .calledWith(new Errors.NotFoundError('not found')) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Documents/DocumentHelperTests.js b/services/web/test/unit/src/Documents/DocumentHelperTests.js new file mode 100644 index 0000000000..0be997e918 --- /dev/null +++ b/services/web/test/unit/src/Documents/DocumentHelperTests.js @@ -0,0 +1,164 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Documents/DocumentHelper.js' +const SandboxedModule = require('sandboxed-module') + +describe('DocumentHelper', function() { + beforeEach(function() { + return (this.DocumentHelper = SandboxedModule.require(modulePath)) + }) + + describe('getTitleFromTexContent', function() { + it('should return the title', function() { + const document = '\\begin{document}\n\\title{foo}\n\\end{document}' + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.equal('foo') + }) + + it('should return the title if surrounded by space', function() { + const document = '\\begin{document}\n \\title{foo} \n\\end{document}' + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.equal('foo') + }) + + it('should return null if there is no title', function() { + const document = '\\begin{document}\n\\end{document}' + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.eql(null) + }) + + it('should accept an array', function() { + const document = ['\\begin{document}', '\\title{foo}', '\\end{document}'] + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.equal('foo') + }) + + it('should parse out formatting elements from the title', function() { + const document = '\\title{\\textbf{\\large{Second Year LaTeX Exercise}}}' + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.equal('Second Year LaTeX Exercise') + }) + + it('should ignore junk after the title', function() { + const document = '\\title{wombat} potato' + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.equal('wombat') + }) + + it('should ignore junk before the title', function() { + const document = + '% this is something that v1 relied on, even though it seems odd \\title{wombat}' + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.equal('wombat') + }) + + // NICETOHAVE: Current implementation doesn't do this + // it "should keep content that surrounds formatting elements", -> + // document = "\\title{Second Year \\large{LaTeX} Exercise}" + // expect(@DocumentHelper.getTitleFromTexContent(document)).to.equal "Second Year LaTeX Exercise" + + return it('should collapse whitespace', function() { + const document = '\\title{Second Year LaTeX Exercise}' + return expect( + this.DocumentHelper.getTitleFromTexContent(document) + ).to.equal('Second Year LaTeX Exercise') + }) + }) + + describe('detex', function() { + // note, there are a number of tests for getTitleFromTexContent that also test cases here + it('leaves a non-TeX string unchanged', function() { + expect(this.DocumentHelper.detex('')).to.equal('') + expect(this.DocumentHelper.detex('a')).to.equal('a') + return expect(this.DocumentHelper.detex('a a')).to.equal('a a') + }) + + it('collapses spaces', function() { + expect(this.DocumentHelper.detex('a a')).to.equal('a a') + return expect(this.DocumentHelper.detex('a \n a')).to.equal('a \n a') + }) + + it('replaces named commands', function() { + expect(this.DocumentHelper.detex('\\LaTeX')).to.equal('LaTeX') + expect(this.DocumentHelper.detex('\\TikZ')).to.equal('TikZ') + expect(this.DocumentHelper.detex('\\TeX')).to.equal('TeX') + return expect(this.DocumentHelper.detex('\\BibTeX')).to.equal('BibTeX') + }) + + it('removes general commands', function() { + expect(this.DocumentHelper.detex('\\foo')).to.equal('') + expect(this.DocumentHelper.detex('\\foo{}')).to.equal('') + expect(this.DocumentHelper.detex('\\foo~Test')).to.equal('Test') + expect(this.DocumentHelper.detex('\\"e')).to.equal('e') + return expect(this.DocumentHelper.detex('\\textit{e}')).to.equal('e') + }) + + it('leaves basic math', function() { + return expect(this.DocumentHelper.detex('$\\cal{O}(n^2)$')).to.equal( + 'O(n^2)' + ) + }) + + return it('removes line spacing commands', function() { + return expect(this.DocumentHelper.detex('a \\\\[1.50cm] b')).to.equal( + 'a b' + ) + }) + }) + + return describe('contentHasDocumentclass', function() { + it('should return true if the content has a documentclass', function() { + const document = ['% line', '% line', '% line', '\\documentclass'] + return expect( + this.DocumentHelper.contentHasDocumentclass(document) + ).to.equal(true) + }) + + it('should allow whitespace before the documentclass', function() { + const document = ['% line', '% line', '% line', ' \\documentclass'] + return expect( + this.DocumentHelper.contentHasDocumentclass(document) + ).to.equal(true) + }) + + it('should not allow non-whitespace before the documentclass', function() { + const document = [ + '% line', + '% line', + '% line', + ' asdf \\documentclass' + ] + return expect( + this.DocumentHelper.contentHasDocumentclass(document) + ).to.equal(false) + }) + + return it('should return false when there is no documentclass', function() { + const document = ['% line', '% line', '% line'] + return expect( + this.DocumentHelper.contentHasDocumentclass(document) + ).to.equal(false) + }) + }) +}) diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsControllerTests.js b/services/web/test/unit/src/Downloads/ProjectDownloadsControllerTests.js new file mode 100644 index 0000000000..ea6ab18638 --- /dev/null +++ b/services/web/test/unit/src/Downloads/ProjectDownloadsControllerTests.js @@ -0,0 +1,177 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Downloads/ProjectDownloadsController.js' +const SandboxedModule = require('sandboxed-module') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') + +describe('ProjectDownloadsController', function() { + beforeEach(function() { + this.project_id = 'project-id-123' + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.DocumentUpdaterHandler = sinon.stub() + return (this.ProjectDownloadsController = SandboxedModule.require( + modulePath, + { + requires: { + './ProjectZipStreamManager': (this.ProjectZipStreamManager = {}), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + 'metrics-sharelatex': (this.metrics = {}), + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + '../DocumentUpdater/DocumentUpdaterHandler': this + .DocumentUpdaterHandler + } + } + )) + }) + + describe('downloadProject', function() { + beforeEach(function() { + this.stream = { pipe: sinon.stub() } + this.ProjectZipStreamManager.createZipStreamForProject = sinon + .stub() + .callsArgWith(1, null, this.stream) + this.req.params = { Project_id: this.project_id } + this.res.contentType = sinon.stub() + this.res.header = sinon.stub() + this.project_name = 'project name with accênts' + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, { name: this.project_name }) + this.DocumentUpdaterHandler.flushProjectToMongo = sinon + .stub() + .callsArgWith(1) + this.metrics.inc = sinon.stub() + return this.ProjectDownloadsController.downloadProject( + this.req, + this.res, + this.next + ) + }) + + it('should create a zip from the project', function() { + return this.ProjectZipStreamManager.createZipStreamForProject + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should stream the zip to the request', function() { + return this.stream.pipe.calledWith(this.res).should.equal(true) + }) + + it('should set the correct content type on the request', function() { + return this.res.contentType + .calledWith('application/zip') + .should.equal(true) + }) + + it('should flush the project to mongo', function() { + return this.DocumentUpdaterHandler.flushProjectToMongo + .calledWith(this.project_id) + .should.equal(true) + }) + + it("should look up the project's name", function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { name: true }) + .should.equal(true) + }) + + it('should name the downloaded file after the project', function() { + return this.res.setContentDisposition + .calledWith('attachment', { filename: `${this.project_name}.zip` }) + .should.equal(true) + }) + + it('should record the action via Metrics', function() { + return this.metrics.inc.calledWith('zip-downloads').should.equal(true) + }) + + return it('should log the action', function() { + return this.logger.log + .calledWith(sinon.match.any, 'downloading project') + .should.equal(true) + }) + }) + + return describe('downloadMultipleProjects', function() { + beforeEach(function() { + this.stream = { pipe: sinon.stub() } + this.ProjectZipStreamManager.createZipStreamForMultipleProjects = sinon + .stub() + .callsArgWith(1, null, this.stream) + this.project_ids = ['project-1', 'project-2'] + this.req.query = { project_ids: this.project_ids.join(',') } + this.res.contentType = sinon.stub() + this.res.header = sinon.stub() + this.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon + .stub() + .callsArgWith(1) + this.metrics.inc = sinon.stub() + return this.ProjectDownloadsController.downloadMultipleProjects( + this.req, + this.res, + this.next + ) + }) + + it('should create a zip from the project', function() { + return this.ProjectZipStreamManager.createZipStreamForMultipleProjects + .calledWith(this.project_ids) + .should.equal(true) + }) + + it('should stream the zip to the request', function() { + return this.stream.pipe.calledWith(this.res).should.equal(true) + }) + + it('should set the correct content type on the request', function() { + return this.res.contentType + .calledWith('application/zip') + .should.equal(true) + }) + + it('should flush the projects to mongo', function() { + return this.DocumentUpdaterHandler.flushMultipleProjectsToMongo + .calledWith(this.project_ids) + .should.equal(true) + }) + + it('should name the downloaded file after the project', function() { + return this.res.setContentDisposition + .calledWith('attachment', { + filename: 'Overleaf Projects (2 items).zip' + }) + .should.equal(true) + }) + + it('should record the action via Metrics', function() { + return this.metrics.inc + .calledWith('zip-downloads-multiple') + .should.equal(true) + }) + + return it('should log the action', function() { + return this.logger.log + .calledWith(sinon.match.any, 'downloading multiple projects') + .should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Downloads/ProjectZipStreamManagerTests.js b/services/web/test/unit/src/Downloads/ProjectZipStreamManagerTests.js new file mode 100644 index 0000000000..61c4e843cf --- /dev/null +++ b/services/web/test/unit/src/Downloads/ProjectZipStreamManagerTests.js @@ -0,0 +1,370 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, + one-var, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS201: Simplify complex destructure assignments + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Downloads/ProjectZipStreamManager.js' +const SandboxedModule = require('sandboxed-module') +const { EventEmitter } = require('events') + +describe('ProjectZipStreamManager', function() { + beforeEach(function() { + this.project_id = 'project-id-123' + this.callback = sinon.stub() + this.archive = { + on() {}, + append: sinon.stub() + } + return (this.ProjectZipStreamManager = SandboxedModule.require(modulePath, { + requires: { + archiver: (this.archiver = sinon.stub().returns(this.archive)), + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + log: sinon.stub() + }), + '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), + '../FileStore/FileStoreHandler': (this.FileStoreHandler = {}), + '../Project/ProjectGetter': (this.ProjectGetter = {}) + } + })) + }) + + describe('createZipStreamForMultipleProjects', () => + describe('successfully', function() { + beforeEach(function(done) { + this.project_ids = ['project-1', 'project-2'] + this.zip_streams = { + 'project-1': new EventEmitter(), + 'project-2': new EventEmitter() + } + + this.project_names = { + 'project-1': 'Project One Name', + 'project-2': 'Project Two Name' + } + + this.ProjectZipStreamManager.createZipStreamForProject = ( + project_id, + callback + ) => { + callback(null, this.zip_streams[project_id]) + setTimeout(() => { + return this.zip_streams[project_id].emit('end') + }) + return 0 + } + sinon.spy(this.ProjectZipStreamManager, 'createZipStreamForProject') + + this.ProjectGetter.getProject = (project_id, fields, callback) => { + return callback(null, { name: this.project_names[project_id] }) + } + sinon.spy(this.ProjectGetter, 'getProject') + + this.ProjectZipStreamManager.createZipStreamForMultipleProjects( + this.project_ids, + (...args) => { + return this.callback(...Array.from(args || [])) + } + ) + + return (this.archive.finalize = () => done()) + }) + + it('should create a zip archive', function() { + return this.archiver.calledWith('zip').should.equal(true) + }) + + it('should return a stream before any processing is done', function() { + this.callback + .calledWith(sinon.match.falsy, this.archive) + .should.equal(true) + return this.callback + .calledBefore(this.ProjectZipStreamManager.createZipStreamForProject) + .should.equal(true) + }) + + it('should get a zip stream for all of the projects', function() { + return Array.from(this.project_ids).map(project_id => + this.ProjectZipStreamManager.createZipStreamForProject + .calledWith(project_id) + .should.equal(true) + ) + }) + + it('should get the names of each project', function() { + return Array.from(this.project_ids).map(project_id => + this.ProjectGetter.getProject + .calledWith(project_id, { name: true }) + .should.equal(true) + ) + }) + + return it('should add all of the projects to the zip', function() { + return Array.from(this.project_ids).map(project_id => + this.archive.append + .calledWith(this.zip_streams[project_id], { + name: this.project_names[project_id] + '.zip' + }) + .should.equal(true) + ) + }) + })) + + describe('createZipStreamForProject', function() { + describe('successfully', function() { + beforeEach(function() { + this.ProjectZipStreamManager.addAllDocsToArchive = sinon + .stub() + .callsArg(2) + this.ProjectZipStreamManager.addAllFilesToArchive = sinon + .stub() + .callsArg(2) + this.archive.finalize = sinon.stub() + return this.ProjectZipStreamManager.createZipStreamForProject( + this.project_id, + this.callback + ) + }) + + it('should create a zip archive', function() { + return this.archiver.calledWith('zip').should.equal(true) + }) + + it('should return a stream before any processing is done', function() { + this.callback + .calledWith(sinon.match.falsy, this.archive) + .should.equal(true) + this.callback + .calledBefore(this.ProjectZipStreamManager.addAllDocsToArchive) + .should.equal(true) + return this.callback + .calledBefore(this.ProjectZipStreamManager.addAllFilesToArchive) + .should.equal(true) + }) + + it('should add all of the project docs to the zip', function() { + return this.ProjectZipStreamManager.addAllDocsToArchive + .calledWith(this.project_id, this.archive) + .should.equal(true) + }) + + it('should add all of the project files to the zip', function() { + return this.ProjectZipStreamManager.addAllFilesToArchive + .calledWith(this.project_id, this.archive) + .should.equal(true) + }) + + return it('should finalise the stream', function() { + return this.archive.finalize.called.should.equal(true) + }) + }) + + describe('with an error adding docs', function() { + beforeEach(function() { + this.ProjectZipStreamManager.addAllDocsToArchive = sinon + .stub() + .callsArgWith(2, new Error('something went wrong')) + this.ProjectZipStreamManager.addAllFilesToArchive = sinon + .stub() + .callsArg(2) + this.archive.finalize = sinon.stub() + return this.ProjectZipStreamManager.createZipStreamForProject( + this.project_id, + this.callback + ) + }) + + it('should log out an error', function() { + return this.logger.error + .calledWith(sinon.match.any, 'error adding docs to zip stream') + .should.equal(true) + }) + + return it('should continue with the process', function() { + this.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( + true + ) + this.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( + true + ) + return this.archive.finalize.called.should.equal(true) + }) + }) + + return describe('with an error adding files', function() { + beforeEach(function() { + this.ProjectZipStreamManager.addAllDocsToArchive = sinon + .stub() + .callsArg(2) + this.ProjectZipStreamManager.addAllFilesToArchive = sinon + .stub() + .callsArgWith(2, new Error('something went wrong')) + this.archive.finalize = sinon.stub() + return this.ProjectZipStreamManager.createZipStreamForProject( + this.project_id, + this.callback + ) + }) + + it('should log out an error', function() { + return this.logger.error + .calledWith(sinon.match.any, 'error adding files to zip stream') + .should.equal(true) + }) + + return it('should continue with the process', function() { + this.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( + true + ) + this.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( + true + ) + return this.archive.finalize.called.should.equal(true) + }) + }) + }) + + describe('addAllDocsToArchive', function() { + beforeEach(function(done) { + this.docs = { + '/main.tex': { + lines: [ + '\\documentclass{article}', + '\\begin{document}', + 'Hello world', + '\\end{document}' + ] + }, + '/chapters/chapter1.tex': { + lines: ['chapter1', 'content'] + } + } + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + return this.ProjectZipStreamManager.addAllDocsToArchive( + this.project_id, + this.archive, + error => { + this.callback(error) + return done() + } + ) + }) + + it('should get the docs for the project', function() { + return this.ProjectEntityHandler.getAllDocs + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should add each doc to the archive', function() { + return (() => { + const result = [] + for (let path in this.docs) { + const doc = this.docs[path] + path = path.slice(1) // remove "/" + result.push( + this.archive.append + .calledWith(doc.lines.join('\n'), { name: path }) + .should.equal(true) + ) + } + return result + })() + }) + }) + + return describe('addAllFilesToArchive', function() { + beforeEach(function() { + this.files = { + '/image.png': { + _id: 'file-id-1' + }, + '/folder/picture.png': { + _id: 'file-id-2' + } + } + this.streams = { + 'file-id-1': new EventEmitter(), + 'file-id-2': new EventEmitter() + } + this.ProjectEntityHandler.getAllFiles = sinon + .stub() + .callsArgWith(1, null, this.files) + this.FileStoreHandler.getFileStream = (project_id, file_id, ...rest) => { + const obj = rest[0], + callback = rest[1] + return callback(null, this.streams[file_id]) + } + sinon.spy(this.FileStoreHandler, 'getFileStream') + this.ProjectZipStreamManager.addAllFilesToArchive( + this.project_id, + this.archive, + this.callback + ) + return (() => { + const result = [] + for (let path in this.streams) { + const stream = this.streams[path] + result.push(stream.emit('end')) + } + return result + })() + }) + + it('should get the files for the project', function() { + return this.ProjectEntityHandler.getAllFiles + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should get a stream for each file', function() { + return (() => { + const result = [] + for (let path in this.files) { + const file = this.files[path] + result.push( + this.FileStoreHandler.getFileStream + .calledWith(this.project_id, file._id) + .should.equal(true) + ) + } + return result + })() + }) + + return it('should add each file to the archive', function() { + return (() => { + const result = [] + for (let path in this.files) { + const file = this.files[path] + path = path.slice(1) // remove "/" + result.push( + this.archive.append + .calledWith(this.streams[file._id], { name: path }) + .should.equal(true) + ) + } + return result + })() + }) + }) +}) diff --git a/services/web/test/unit/src/Editor/EditorControllerTests.js b/services/web/test/unit/src/Editor/EditorControllerTests.js new file mode 100644 index 0000000000..e7a00119fe --- /dev/null +++ b/services/web/test/unit/src/Editor/EditorControllerTests.js @@ -0,0 +1,977 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const { expect } = require('chai') + +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Editor/EditorController' +) +const MockClient = require('../helpers/MockClient') +const assert = require('assert') + +describe('EditorController', function() { + beforeEach(function() { + this.project_id = 'test-project-id' + this.source = 'dropbox' + + this.doc = { _id: (this.doc_id = 'test-doc-id') } + this.docName = 'doc.tex' + this.docLines = ['1234', 'dskl'] + this.file = { _id: (this.file_id = 'dasdkjk') } + this.fileName = 'file.png' + this.fsPath = '/folder/file.png' + this.linkedFileData = { provider: 'url' } + + this.newFile = { _id: 'new-file-id' } + + this.folder_id = '123ksajdn' + this.folder = { _id: this.folder_id } + this.folderName = 'folder' + + this.callback = sinon.stub() + + return (this.EditorController = SandboxedModule.require(modulePath, { + requires: { + '../Project/ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}), + '../Project/ProjectOptionsHandler': (this.ProjectOptionsHandler = { + setCompiler: sinon.stub().yields(), + setImageName: sinon.stub().yields(), + setSpellCheckLanguage: sinon.stub().yields() + }), + '../Project/ProjectDetailsHandler': (this.ProjectDetailsHandler = { + setProjectDescription: sinon.stub().yields(), + renameProject: sinon.stub().yields(), + setPublicAccessLevel: sinon.stub().yields() + }), + '../Project/ProjectDeleter': (this.ProjectDeleter = {}), + '../DocumentUpdater/DocumentUpdaterHandler': (this.DocumentUpdaterHandler = { + flushDocToMongo: sinon.stub().yields(), + setDocument: sinon.stub().yields() + }), + './EditorRealTimeController': (this.EditorRealTimeController = { + emitToRoom: sinon.stub() + }), + 'metrics-sharelatex': (this.Metrics = { inc: sinon.stub() }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }) + } + })) + }) + + describe('addDoc', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.addDocWithRanges = sinon + .stub() + .yields(null, this.doc, this.folder_id) + return this.EditorController.addDoc( + this.project_id, + this.folder_id, + this.docName, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + it('should add the doc using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.addDocWithRanges + .calledWith( + this.project_id, + this.folder_id, + this.docName, + this.docLines, + {} + ) + .should.equal(true) + }) + + it('should send the update out to the users in the project', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewDoc', + this.folder_id, + this.doc, + this.source + ) + .should.equal(true) + }) + + return it('calls the callback', function() { + return this.callback.calledWith(null, this.doc).should.equal(true) + }) + }) + + describe('addFile', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.addFile = sinon + .stub() + .yields(null, this.file, this.folder_id) + return this.EditorController.addFile( + this.project_id, + this.folder_id, + this.fileName, + this.fsPath, + this.linkedFileData, + this.source, + this.user_id, + this.callback + ) + }) + + it('should add the folder using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.addFile + .calledWith( + this.project_id, + this.folder_id, + this.fileName, + this.fsPath, + this.linkedFileData, + this.user_id + ) + .should.equal(true) + }) + + it('should send the update of a new folder out to the users in the project', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewFile', + this.folder_id, + this.file, + this.source, + this.linkedFileData + ) + .should.equal(true) + }) + + return it('calls the callback', function() { + return this.callback.calledWith(null, this.file).should.equal(true) + }) + }) + + describe('upsertDoc', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.upsertDoc = sinon + .stub() + .yields(null, this.doc, false) + return this.EditorController.upsertDoc( + this.project_id, + this.folder_id, + this.docName, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + it('upserts the doc using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.upsertDoc + .calledWith( + this.project_id, + this.folder_id, + this.docName, + this.docLines, + this.source + ) + .should.equal(true) + }) + + it('returns the doc', function() { + return this.callback.calledWith(null, this.doc).should.equal(true) + }) + + return describe('doc does not exist', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.upsertDoc = sinon + .stub() + .yields(null, this.doc, true) + return this.EditorController.upsertDoc( + this.project_id, + this.folder_id, + this.docName, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + return it('sends an update out to users in the project', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewDoc', + this.folder_id, + this.doc, + this.source + ) + .should.equal(true) + }) + }) + }) + + describe('upsertFile', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.upsertFile = sinon + .stub() + .yields(null, this.newFile, false, this.file) + return this.EditorController.upsertFile( + this.project_id, + this.folder_id, + this.fileName, + this.fsPath, + this.linkedFileData, + this.source, + this.user_id, + this.callback + ) + }) + + it('upserts the file using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.upsertFile + .calledWith( + this.project_id, + this.folder_id, + this.fileName, + this.fsPath, + this.linkedFileData, + this.user_id + ) + .should.equal(true) + }) + + it('returns the file', function() { + return this.callback.calledWith(null, this.newFile).should.equal(true) + }) + + return describe('file does not exist', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.upsertFile = sinon + .stub() + .yields(null, this.file, true) + return this.EditorController.upsertFile( + this.project_id, + this.folder_id, + this.fileName, + this.fsPath, + this.linkedFileData, + this.source, + this.user_id, + this.callback + ) + }) + + return it('should send the update out to users in the project', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewFile', + this.folder_id, + this.file, + this.source, + this.linkedFileData + ) + .should.equal(true) + }) + }) + }) + + describe('upsertDocWithPath', function() { + beforeEach(function() { + this.docPath = '/folder/doc' + + this.ProjectEntityUpdateHandler.upsertDocWithPath = sinon + .stub() + .yields(null, this.doc, false, [], this.folder) + return this.EditorController.upsertDocWithPath( + this.project_id, + this.docPath, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + it('upserts the doc using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.upsertDocWithPath + .calledWith(this.project_id, this.docPath, this.docLines, this.source) + .should.equal(true) + }) + + describe('doc does not exist', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.upsertDocWithPath = sinon + .stub() + .yields(null, this.doc, true, [], this.folder) + return this.EditorController.upsertDocWithPath( + this.project_id, + this.docPath, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + return it('should send the update for the doc out to users in the project', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewDoc', + this.folder_id, + this.doc, + this.source + ) + .should.equal(true) + }) + }) + + return describe('folders required for doc do not exist', function() { + beforeEach(function() { + const folders = [ + (this.folderA = { _id: 2, parentFolder_id: 1 }), + (this.folderB = { _id: 3, parentFolder_id: 2 }) + ] + this.ProjectEntityUpdateHandler.upsertDocWithPath = sinon + .stub() + .yields(null, this.doc, true, folders, this.folderB) + return this.EditorController.upsertDocWithPath( + this.project_id, + this.docPath, + this.docLines, + this.source, + this.user_id, + this.callback + ) + }) + + return it('should send the update for each folder to users in the project', function() { + this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewFolder', + this.folderA.parentFolder_id, + this.folderA + ) + .should.equal(true) + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewFolder', + this.folderB.parentFolder_id, + this.folderB + ) + .should.equal(true) + }) + }) + }) + + describe('upsertFileWithPath', function() { + beforeEach(function() { + this.filePath = '/folder/file' + + this.ProjectEntityUpdateHandler.upsertFileWithPath = sinon + .stub() + .yields(null, this.newFile, false, this.file, [], this.folder) + return this.EditorController.upsertFileWithPath( + this.project_id, + this.filePath, + this.fsPath, + this.linkedFileData, + this.source, + this.user_id, + this.callback + ) + }) + + it('upserts the file using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.upsertFileWithPath + .calledWith( + this.project_id, + this.filePath, + this.fsPath, + this.linkedFileData + ) + .should.equal(true) + }) + + describe('file does not exist', function() { + beforeEach(function() { + this.ProjectEntityUpdateHandler.upsertFileWithPath = sinon + .stub() + .yields(null, this.file, true, undefined, [], this.folder) + return this.EditorController.upsertFileWithPath( + this.project_id, + this.filePath, + this.fsPath, + this.linkedFileData, + this.source, + this.user_id, + this.callback + ) + }) + + return it('should send the update for the file out to users in the project', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewFile', + this.folder_id, + this.file, + this.source, + this.linkedFileData + ) + .should.equal(true) + }) + }) + + return describe('folders required for file do not exist', function() { + beforeEach(function() { + const folders = [ + (this.folderA = { _id: 2, parentFolder_id: 1 }), + (this.folderB = { _id: 3, parentFolder_id: 2 }) + ] + this.ProjectEntityUpdateHandler.upsertFileWithPath = sinon + .stub() + .yields(null, this.file, true, undefined, folders, this.folderB) + return this.EditorController.upsertFileWithPath( + this.project_id, + this.filePath, + this.fsPath, + this.linkedFileData, + this.source, + this.user_id, + this.callback + ) + }) + + return it('should send the update for each folder to users in the project', function() { + this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewFolder', + this.folderA.parentFolder_id, + this.folderA + ) + .should.equal(true) + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveNewFolder', + this.folderB.parentFolder_id, + this.folderB + ) + .should.equal(true) + }) + }) + }) + + describe('addFolder', function() { + beforeEach(function() { + this.EditorController._notifyProjectUsersOfNewFolder = sinon + .stub() + .yields() + this.ProjectEntityUpdateHandler.addFolder = sinon + .stub() + .yields(null, this.folder, this.folder_id) + return this.EditorController.addFolder( + this.project_id, + this.folder_id, + this.folderName, + this.source, + this.callback + ) + }) + + it('should add the folder using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.addFolder + .calledWith(this.project_id, this.folder_id, this.folderName) + .should.equal(true) + }) + + it('should notifyProjectUsersOfNewFolder', function() { + return this.EditorController._notifyProjectUsersOfNewFolder.calledWith( + this.project_id, + this.folder_id, + this.folder + ) + }) + + return it('should return the folder in the callback', function() { + return this.callback.calledWith(null, this.folder).should.equal(true) + }) + }) + + describe('mkdirp', function() { + beforeEach(function() { + this.path = 'folder1/folder2' + this.folders = [ + (this.folderA = { _id: 2, parentFolder_id: 1 }), + (this.folderB = { _id: 3, parentFolder_id: 2 }) + ] + this.EditorController._notifyProjectUsersOfNewFolders = sinon + .stub() + .yields() + this.ProjectEntityUpdateHandler.mkdirp = sinon + .stub() + .yields(null, this.folders, this.folder) + return this.EditorController.mkdirp( + this.project_id, + this.path, + this.callback + ) + }) + + it('should create the folder using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.mkdirp + .calledWith(this.project_id, this.path) + .should.equal(true) + }) + + it('should notifyProjectUsersOfNewFolder', function() { + return this.EditorController._notifyProjectUsersOfNewFolders.calledWith( + this.project_id, + this.folders + ) + }) + + return it('should return the folder in the callback', function() { + return this.callback + .calledWith(null, this.folders, this.folder) + .should.equal(true) + }) + }) + + describe('deleteEntity', function() { + beforeEach(function() { + this.entity_id = 'entity_id_here' + this.type = 'doc' + this.ProjectEntityUpdateHandler.deleteEntity = sinon.stub().yields() + return this.EditorController.deleteEntity( + this.project_id, + this.entity_id, + this.type, + this.source, + this.user_id, + this.callback + ) + }) + + it('should delete the folder using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.deleteEntity.calledWith( + this.project_id, + this.entity_id, + this.type, + this.user_id + ).should.equal.true + }) + + return it('notify users an entity has been deleted', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'removeEntity', + this.entity_id, + this.source + ) + .should.equal(true) + }) + }) + + describe('deleteEntityWithPath', function() { + beforeEach(function() { + this.entity_id = 'entity_id_here' + this.ProjectEntityUpdateHandler.deleteEntityWithPath = sinon + .stub() + .yields(null, this.entity_id) + this.path = 'folder1/folder2' + return this.EditorController.deleteEntityWithPath( + this.project_id, + this.path, + this.source, + this.user_id, + this.callback + ) + }) + + it('should delete the folder using the project entity handler', function() { + return this.ProjectEntityUpdateHandler.deleteEntityWithPath.calledWith( + this.project_id, + this.path, + this.user_id + ).should.equal.true + }) + + return it('notify users an entity has been deleted', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'removeEntity', + this.entity_id, + this.source + ) + .should.equal(true) + }) + }) + + describe('notifyUsersProjectHasBeenDeletedOrRenamed', () => + it('should emmit a message to all users in a project', function(done) { + return this.EditorController.notifyUsersProjectHasBeenDeletedOrRenamed( + this.project_id, + err => { + this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'projectRenamedOrDeletedByExternalSource' + ) + .should.equal(true) + return done() + } + ) + })) + + describe('updateProjectDescription', function() { + beforeEach(function() { + this.description = 'new description' + return this.EditorController.updateProjectDescription( + this.project_id, + this.description, + this.callback + ) + }) + + it('should send the new description to the project details handler', function() { + return this.ProjectDetailsHandler.setProjectDescription + .calledWith(this.project_id, this.description) + .should.equal(true) + }) + + return it('should notify the other clients about the updated description', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'projectDescriptionUpdated', + this.description + ) + .should.equal(true) + }) + }) + + describe('deleteProject', function() { + beforeEach(function() { + this.err = 'errro' + return (this.ProjectDeleter.deleteProject = sinon + .stub() + .callsArgWith(1, this.err)) + }) + + return it('should call the project handler', function(done) { + return this.EditorController.deleteProject(this.project_id, err => { + err.should.equal(this.err) + this.ProjectDeleter.deleteProject + .calledWith(this.project_id) + .should.equal(true) + return done() + }) + }) + }) + + describe('renameEntity', function() { + beforeEach(function(done) { + this.entity_id = 'entity_id_here' + this.entityType = 'doc' + this.newName = 'bobsfile.tex' + this.ProjectEntityUpdateHandler.renameEntity = sinon.stub().yields() + + return this.EditorController.renameEntity( + this.project_id, + this.entity_id, + this.entityType, + this.newName, + this.user_id, + done + ) + }) + + it('should call the project handler', function() { + return this.ProjectEntityUpdateHandler.renameEntity + .calledWith( + this.project_id, + this.entity_id, + this.entityType, + this.newName, + this.user_id + ) + .should.equal(true) + }) + + return it('should emit the update to the room', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveEntityRename', + this.entity_id, + this.newName + ) + .should.equal(true) + }) + }) + + describe('moveEntity', function() { + beforeEach(function() { + this.entity_id = 'entity_id_here' + this.entityType = 'doc' + this.ProjectEntityUpdateHandler.moveEntity = sinon.stub().yields() + return this.EditorController.moveEntity( + this.project_id, + this.entity_id, + this.folder_id, + this.entityType, + this.user_id, + this.callback + ) + }) + + it('should call the ProjectEntityUpdateHandler', function() { + return this.ProjectEntityUpdateHandler.moveEntity + .calledWith( + this.project_id, + this.entity_id, + this.folder_id, + this.entityType, + this.user_id + ) + .should.equal(true) + }) + + it('should emit the update to the room', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'reciveEntityMove', + this.entity_id, + this.folder_id + ) + .should.equal(true) + }) + + return it('calls the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('renameProject', function() { + beforeEach(function() { + this.err = 'errro' + this.newName = 'new name here' + return this.EditorController.renameProject( + this.project_id, + this.newName, + this.callback + ) + }) + + it('should call the EditorController', function() { + return this.ProjectDetailsHandler.renameProject + .calledWith(this.project_id, this.newName) + .should.equal(true) + }) + + return it('should emit the update to the room', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'projectNameUpdated', this.newName) + .should.equal(true) + }) + }) + + describe('setCompiler', function() { + beforeEach(function() { + this.compiler = 'latex' + return this.EditorController.setCompiler( + this.project_id, + this.compiler, + this.callback + ) + }) + + return it('should send the new compiler and project id to the project options handler', function() { + this.ProjectOptionsHandler.setCompiler + .calledWith(this.project_id, this.compiler) + .should.equal(true) + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'compilerUpdated', this.compiler) + .should.equal(true) + }) + }) + + describe('setImageName', function() { + beforeEach(function() { + this.imageName = 'texlive-1234.5' + return this.EditorController.setImageName( + this.project_id, + this.imageName, + this.callback + ) + }) + + return it('should send the new imageName and project id to the project options handler', function() { + this.ProjectOptionsHandler.setImageName + .calledWith(this.project_id, this.imageName) + .should.equal(true) + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'imageNameUpdated', this.imageName) + .should.equal(true) + }) + }) + + describe('setSpellCheckLanguage', function() { + beforeEach(function() { + this.languageCode = 'fr' + return this.EditorController.setSpellCheckLanguage( + this.project_id, + this.languageCode, + this.callback + ) + }) + + return it('should send the new languageCode and project id to the project options handler', function() { + this.ProjectOptionsHandler.setSpellCheckLanguage + .calledWith(this.project_id, this.languageCode) + .should.equal(true) + return this.EditorRealTimeController.emitToRoom + .calledWith( + this.project_id, + 'spellCheckLanguageUpdated', + this.languageCode + ) + .should.equal(true) + }) + }) + + describe('setPublicAccessLevel', function() { + describe('when setting to private', function() { + beforeEach(function() { + this.newAccessLevel = 'private' + this.ProjectDetailsHandler.ensureTokensArePresent = sinon + .stub() + .yields(null, this.tokens) + return this.EditorController.setPublicAccessLevel( + this.project_id, + this.newAccessLevel, + this.callback + ) + }) + + it('should set the access level', function() { + return this.ProjectDetailsHandler.setPublicAccessLevel + .calledWith(this.project_id, this.newAccessLevel) + .should.equal(true) + }) + + it('should broadcast the access level change', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:publicAccessLevel:changed') + .should.equal(true) + }) + + it('should not ensure tokens are present for project', function() { + return this.ProjectDetailsHandler.ensureTokensArePresent + .calledWith(this.project_id) + .should.equal(false) + }) + + return it('should not broadcast a token change', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:tokens:changed', { + tokens: this.tokens + }) + .should.equal(false) + }) + }) + + return describe('when setting to tokenBased', function() { + beforeEach(function() { + this.newAccessLevel = 'tokenBased' + this.tokens = { readOnly: 'aaa', readAndWrite: '42bbb' } + this.ProjectDetailsHandler.ensureTokensArePresent = sinon + .stub() + .yields(null, this.tokens) + return this.EditorController.setPublicAccessLevel( + this.project_id, + this.newAccessLevel, + this.callback + ) + }) + + it('should set the access level', function() { + return this.ProjectDetailsHandler.setPublicAccessLevel + .calledWith(this.project_id, this.newAccessLevel) + .should.equal(true) + }) + + it('should broadcast the access level change', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:publicAccessLevel:changed') + .should.equal(true) + }) + + it('should ensure tokens are present for project', function() { + return this.ProjectDetailsHandler.ensureTokensArePresent + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should broadcast the token change too', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'project:tokens:changed', { + tokens: this.tokens + }) + .should.equal(true) + }) + }) + }) + + return describe('setRootDoc', function() { + beforeEach(function() { + this.newRootDocID = '21312321321' + this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() + return this.EditorController.setRootDoc( + this.project_id, + this.newRootDocID, + this.callback + ) + }) + + it('should call the ProjectEntityUpdateHandler', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, this.newRootDocID) + .should.equal(true) + }) + + return it('should emit the update to the room', function() { + return this.EditorRealTimeController.emitToRoom + .calledWith(this.project_id, 'rootDocUpdated', this.newRootDocID) + .should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Editor/EditorHttpControllerTests.js b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js new file mode 100644 index 0000000000..a4cf2ad59b --- /dev/null +++ b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js @@ -0,0 +1,468 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Editor/EditorHttpController' +) + +describe('EditorHttpController', function() { + beforeEach(function() { + this.EditorHttpController = SandboxedModule.require(modulePath, { + requires: { + '../Project/ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}), + '../Project/ProjectDeleter': (this.ProjectDeleter = {}), + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../User/UserGetter': (this.UserGetter = {}), + '../Authorization/AuthorizationManager': (this.AuthorizationManager = {}), + '../Project/ProjectEditorHandler': (this.ProjectEditorHandler = {}), + './EditorRealTimeController': (this.EditorRealTimeController = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './EditorController': (this.EditorController = {}), + 'metrics-sharelatex': (this.Metrics = { inc: sinon.stub() }), + '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Collaborators/CollaboratorsInviteHandler': (this.CollaboratorsInviteHandler = {}), + '../TokenAccess/TokenAccessHandler': (this.TokenAccessHandler = {}), + '../Authentication/AuthenticationController': (this.AuthenticationController = {}) + } + }) + + this.project_id = 'mock-project-id' + this.doc_id = 'mock-doc-id' + this.user_id = 'mock-user-id' + this.parent_folder_id = 'mock-folder-id' + this.userId = 1234 + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId) + this.req = {} + this.res = { + send: sinon.stub(), + sendStatus: sinon.stub(), + json: sinon.stub() + } + this.callback = sinon.stub() + this.TokenAccessHandler.getRequestToken = sinon + .stub() + .returns((this.token = null)) + return (this.TokenAccessHandler.protectTokens = sinon.stub()) + }) + + describe('joinProject', function() { + beforeEach(function() { + this.req.params = { Project_id: this.project_id } + this.req.query = { user_id: this.user_id } + this.projectView = { + _id: this.project_id + } + this.EditorHttpController._buildJoinProjectView = sinon + .stub() + .callsArgWith(3, null, this.projectView, 'owner') + return (this.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.EditorHttpController.joinProject(this.req, this.res) + }) + + it('should get the project view', function() { + return this.EditorHttpController._buildJoinProjectView + .calledWith(this.req, this.project_id, this.user_id) + .should.equal(true) + }) + + it('should return the project and privilege level', function() { + return this.res.json + .calledWith({ + project: this.projectView, + privilegeLevel: 'owner' + }) + .should.equal(true) + }) + + it('should not try to unmark the project as deleted', function() { + return this.ProjectDeleter.unmarkAsDeletedByExternalSource.called.should.equal( + false + ) + }) + + return it('should send an inc metric', function() { + return this.Metrics.inc + .calledWith('editor.join-project') + .should.equal(true) + }) + }) + + describe('when the project is marked as deleted', function() { + beforeEach(function() { + this.projectView.deletedByExternalDataSource = true + return this.EditorHttpController.joinProject(this.req, this.res) + }) + + return it('should unmark the project as deleted', function() { + return this.ProjectDeleter.unmarkAsDeletedByExternalSource + .calledWith(this.project_id) + .should.equal(true) + }) + }) + + return describe('with an anonymous user', function() { + beforeEach(function() { + this.req.query = { user_id: 'anonymous-user' } + return this.EditorHttpController.joinProject(this.req, this.res) + }) + + return it('should pass the user id as null', function() { + return this.EditorHttpController._buildJoinProjectView + .calledWith(this.req, this.project_id, null) + .should.equal(true) + }) + }) + }) + + describe('_buildJoinProjectView', function() { + beforeEach(function() { + this.project = { + _id: this.project_id, + owner_ref: { _id: 'something' } + } + this.user = { + _id: (this.user_id = 'user-id'), + projects: {} + } + this.members = ['members', 'mock'] + this.tokenMembers = ['one', 'two'] + this.projectModelView = { + _id: this.project_id, + owner: { _id: 'something' }, + view: true + } + this.invites = [ + { + _id: 'invite_one', + email: 'user-one@example.com', + privileges: 'readOnly', + projectId: this.project._id + }, + { + _id: 'invite_two', + email: 'user-two@example.com', + privileges: 'readOnly', + projectId: this.project._id + } + ] + this.ProjectEditorHandler.buildProjectModelView = sinon + .stub() + .returns(this.projectModelView) + this.ProjectGetter.getProjectWithoutDocLines = sinon + .stub() + .callsArgWith(1, null, this.project) + this.CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels = sinon + .stub() + .callsArgWith(1, null, this.members) + this.CollaboratorsHandler.getTokenMembersWithPrivilegeLevels = sinon + .stub() + .callsArgWith(1, null, this.tokenMembers) + this.CollaboratorsInviteHandler.getAllInvites = sinon + .stub() + .callsArgWith(1, null, this.invites) + return (this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, this.user)) + }) + + describe('when authorized', function() { + beforeEach(function() { + this.AuthorizationManager.getPrivilegeLevelForProject = sinon + .stub() + .callsArgWith(3, null, 'owner') + return this.EditorHttpController._buildJoinProjectView( + this.req, + this.project_id, + this.user_id, + this.callback + ) + }) + + it('should find the project without doc lines', function() { + return this.ProjectGetter.getProjectWithoutDocLines + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should get the list of users in the project', function() { + return this.CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should check the privilege level', function() { + return this.AuthorizationManager.getPrivilegeLevelForProject + .calledWith(this.user_id, this.project_id, this.token) + .should.equal(true) + }) + + it('should include the invites', function() { + return this.CollaboratorsInviteHandler.getAllInvites + .calledWith(this.project._id) + .should.equal(true) + }) + + return it('should return the project model view, privilege level and protocol version', function() { + return this.callback + .calledWith(null, this.projectModelView, 'owner') + .should.equal(true) + }) + }) + + return describe('when not authorized', function() { + beforeEach(function() { + this.AuthorizationManager.getPrivilegeLevelForProject = sinon + .stub() + .callsArgWith(3, null, null) + return this.EditorHttpController._buildJoinProjectView( + this.req, + this.project_id, + this.user_id, + this.callback + ) + }) + + return it('should return false in the callback', function() { + return this.callback.calledWith(null, null, false).should.equal(true) + }) + }) + }) + + describe('addDoc', function() { + beforeEach(function() { + this.doc = { mock: 'doc' } + this.req.params = { Project_id: this.project_id } + this.req.body = { + name: (this.name = 'doc-name'), + parent_folder_id: this.parent_folder_id + } + return (this.EditorController.addDoc = sinon + .stub() + .callsArgWith(6, null, this.doc)) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.EditorHttpController.addDoc(this.req, this.res) + }) + + it('should call EditorController.addDoc', function() { + return this.EditorController.addDoc + .calledWith( + this.project_id, + this.parent_folder_id, + this.name, + [], + 'editor', + this.userId + ) + .should.equal(true) + }) + + return it('should send the doc back as JSON', function() { + return this.res.json.calledWith(this.doc).should.equal(true) + }) + }) + + return describe('unsuccesfully', function() { + beforeEach(function() { + this.req.body.name = '' + return this.EditorHttpController.addDoc(this.req, this.res) + }) + + return it('should send back a bad request status code', function() { + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + }) + }) + + describe('addFolder', function() { + beforeEach(function() { + this.folder = { mock: 'folder' } + this.req.params = { Project_id: this.project_id } + this.req.body = { + name: (this.name = 'folder-name'), + parent_folder_id: this.parent_folder_id + } + return (this.EditorController.addFolder = sinon + .stub() + .callsArgWith(4, null, this.folder)) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.EditorHttpController.addFolder(this.req, this.res) + }) + + it('should call EditorController.addFolder', function() { + return this.EditorController.addFolder + .calledWith( + this.project_id, + this.parent_folder_id, + this.name, + 'editor' + ) + .should.equal(true) + }) + + return it('should send the folder back as JSON', function() { + return this.res.json.calledWith(this.folder).should.equal(true) + }) + }) + + return describe('unsuccesfully', function() { + beforeEach(function() { + this.req.body.name = '' + return this.EditorHttpController.addFolder(this.req, this.res) + }) + + return it('should send back a bad request status code', function() { + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + }) + }) + + describe('renameEntity', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + entity_id: (this.entity_id = 'entity-id-123'), + entity_type: (this.entity_type = 'entity-type') + } + this.req.body = { name: (this.name = 'new-name') } + this.EditorController.renameEntity = sinon.stub().callsArg(5) + return this.EditorHttpController.renameEntity(this.req, this.res) + }) + + it('should call EditorController.renameEntity', function() { + return this.EditorController.renameEntity + .calledWith( + this.project_id, + this.entity_id, + this.entity_type, + this.name, + this.userId + ) + .should.equal(true) + }) + + return it('should send back a success response', function() { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + }) + + describe('renameEntity with long name', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + entity_id: (this.entity_id = 'entity-id-123'), + entity_type: (this.entity_type = 'entity-type') + } + this.req.body = { + name: (this.name = + 'EDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOTEDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOT') + } + this.EditorController.renameEntity = sinon.stub().callsArg(4) + return this.EditorHttpController.renameEntity(this.req, this.res) + }) + + return it('should send back a bad request status code', function() { + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + }) + + describe('rename entity with 0 length name', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + entity_id: (this.entity_id = 'entity-id-123'), + entity_type: (this.entity_type = 'entity-type') + } + this.req.body = { name: (this.name = '') } + this.EditorController.renameEntity = sinon.stub().callsArg(4) + return this.EditorHttpController.renameEntity(this.req, this.res) + }) + + return it('should send back a bad request status code', function() { + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + }) + + describe('moveEntity', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + entity_id: (this.entity_id = 'entity-id-123'), + entity_type: (this.entity_type = 'entity-type') + } + this.req.body = { folder_id: (this.folder_id = 'folder-id-123') } + this.EditorController.moveEntity = sinon.stub().callsArg(5) + return this.EditorHttpController.moveEntity(this.req, this.res) + }) + + it('should call EditorController.moveEntity', function() { + return this.EditorController.moveEntity + .calledWith( + this.project_id, + this.entity_id, + this.folder_id, + this.entity_type, + this.userId + ) + .should.equal(true) + }) + + return it('should send back a success response', function() { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + }) + + return describe('deleteEntity', function() { + beforeEach(function() { + this.req.params = { + Project_id: this.project_id, + entity_id: (this.entity_id = 'entity-id-123'), + entity_type: (this.entity_type = 'entity-type') + } + this.EditorController.deleteEntity = sinon.stub().callsArg(5) + return this.EditorHttpController.deleteEntity(this.req, this.res) + }) + + it('should call EditorController.deleteEntity', function() { + return this.EditorController.deleteEntity + .calledWith( + this.project_id, + this.entity_id, + this.entity_type, + 'editor', + this.userId + ) + .should.equal(true) + }) + + return it('should send back a success response', function() { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Editor/EditorRealTimeControllerTests.js b/services/web/test/unit/src/Editor/EditorRealTimeControllerTests.js new file mode 100644 index 0000000000..b214ca1627 --- /dev/null +++ b/services/web/test/unit/src/Editor/EditorRealTimeControllerTests.js @@ -0,0 +1,88 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Editor/EditorRealTimeController' +) + +describe('EditorRealTimeController', function() { + beforeEach(function() { + this.rclient = { publish: sinon.stub() } + this.EditorRealTimeController = SandboxedModule.require(modulePath, { + requires: { + '../../infrastructure/RedisWrapper': { + client: () => this.rclient + }, + '../../infrastructure/Server': { + io: (this.io = {}) + }, + 'settings-sharelatex': { redis: {} }, + crypto: (this.crypto = { + randomBytes: sinon + .stub() + .withArgs(4) + .returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) + }), + os: (this.os = { hostname: sinon.stub().returns('somehost') }) + } + }) + + this.room_id = 'room-id' + this.message = 'message-to-editor' + return (this.payload = ['argument one', 42]) + }) + + describe('emitToRoom', function() { + beforeEach(function() { + this.message_id = 'web:somehost:01020304-0' + return this.EditorRealTimeController.emitToRoom( + this.room_id, + this.message, + ...Array.from(this.payload) + ) + }) + + return it('should publish the message to redis', function() { + return this.rclient.publish + .calledWith( + 'editor-events', + JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload, + _id: this.message_id + }) + ) + .should.equal(true) + }) + }) + + return describe('emitToAll', function() { + beforeEach(function() { + this.EditorRealTimeController.emitToRoom = sinon.stub() + return this.EditorRealTimeController.emitToAll( + this.message, + ...Array.from(this.payload) + ) + }) + + return it("should emit to the room 'all'", function() { + return this.EditorRealTimeController.emitToRoom + .calledWith('all', this.message, ...Array.from(this.payload)) + .should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Email/EmailBuilderTests.js b/services/web/test/unit/src/Email/EmailBuilderTests.js new file mode 100644 index 0000000000..ae6f9a08ef --- /dev/null +++ b/services/web/test/unit/src/Email/EmailBuilderTests.js @@ -0,0 +1,123 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Email/EmailBuilder' +) +const { expect } = require('chai') +const _ = require('underscore') +_.templateSettings = { interpolate: /\{\{(.+?)\}\}/g } + +describe('EmailBuilder', function() { + beforeEach(function() { + this.settings = { + appName: 'testApp', + brandPrefix: '' + } + return (this.EmailBuilder = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {} + } + } + })) + }) + + describe('projectInvite', function() { + beforeEach(function() { + return (this.opts = { + to: 'bob@bob.com', + first_name: 'bob', + owner: { + email: 'sally@hally.com' + }, + inviteUrl: 'http://example.com/invite', + project: { + url: 'http://www.project.com', + name: 'standard project' + } + }) + }) + + describe('when sending a normal email', function() { + beforeEach(function() { + return (this.email = this.EmailBuilder.buildEmail( + 'projectInvite', + this.opts + )) + }) + + it('should have html and text properties', function() { + expect(this.email.html != null).to.equal(true) + return expect(this.email.text != null).to.equal(true) + }) + + return it('should not have undefined in it', function() { + this.email.html.indexOf('undefined').should.equal(-1) + return this.email.subject.indexOf('undefined').should.equal(-1) + }) + }) + + return describe('when someone is up to no good', function() { + beforeEach(function() { + this.opts.project.name = "" + return (this.email = this.EmailBuilder.buildEmail( + 'projectInvite', + this.opts + )) + }) + + it('should not contain unescaped html in the html part', function() { + return expect(this.email.html).to.contain('New Project') + }) + + return it('should not have undefined in it', function() { + this.email.html.indexOf('undefined').should.equal(-1) + return this.email.subject.indexOf('undefined').should.equal(-1) + }) + }) + }) + + return describe('SpamSafe', function() { + beforeEach(function() { + this.opts = { + to: 'bob@joe.com', + first_name: 'bob', + owner: { + email: 'sally@hally.com' + }, + inviteUrl: 'http://example.com/invite', + project: { + url: 'http://www.project.com', + name: 'come buy my product at http://notascam.com' + } + } + return (this.email = this.EmailBuilder.buildEmail( + 'projectInvite', + this.opts + )) + }) + + return it('should replace spammy project name', function() { + this.email.html.indexOf('a new project').should.not.equal(-1) + return this.email.subject.indexOf('New Project').should.not.equal(-1) + }) + }) +}) diff --git a/services/web/test/unit/src/Email/EmailHandlerTests.js b/services/web/test/unit/src/Email/EmailHandlerTests.js new file mode 100644 index 0000000000..c131a9cd37 --- /dev/null +++ b/services/web/test/unit/src/Email/EmailHandlerTests.js @@ -0,0 +1,122 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Email/EmailHandler' +) +const { expect } = require('chai') + +describe('EmailHandler', function() { + beforeEach(function() { + this.settings = { email: {} } + this.EmailBuilder = { buildEmail: sinon.stub() } + this.EmailSender = { sendEmail: sinon.stub() } + this.EmailHandler = SandboxedModule.require(modulePath, { + requires: { + './EmailBuilder': this.EmailBuilder, + './EmailSender': this.EmailSender, + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {} + } + } + }) + + return (this.html = 'hello') + }) + + return describe('send email', function() { + it('should use the correct options', function(done) { + this.EmailBuilder.buildEmail.returns({ html: this.html }) + this.EmailSender.sendEmail.callsArgWith(1) + + const opts = { to: 'bob@bob.com' } + return this.EmailHandler.sendEmail('welcome', opts, () => { + const args = this.EmailSender.sendEmail.args[0][0] + args.html.should.equal(this.html) + return done() + }) + }) + + it('should return the erroor', function(done) { + this.EmailBuilder.buildEmail.returns({ html: this.html }) + this.EmailSender.sendEmail.callsArgWith(1, 'error') + + const opts = { + to: 'bob@bob.com', + subject: 'hello bob' + } + return this.EmailHandler.sendEmail('welcome', opts, err => { + err.should.equal('error') + return done() + }) + }) + + it('should not send an email if lifecycle is not enabled', function(done) { + this.settings.email.lifecycle = false + this.EmailBuilder.buildEmail.returns({ type: 'lifecycle' }) + return this.EmailHandler.sendEmail('welcome', {}, () => { + this.EmailSender.sendEmail.called.should.equal(false) + return done() + }) + }) + + it('should send an email if lifecycle is not enabled but the type is notification', function(done) { + this.settings.email.lifecycle = false + this.EmailBuilder.buildEmail.returns({ type: 'notification' }) + this.EmailSender.sendEmail.callsArgWith(1) + const opts = { to: 'bob@bob.com' } + return this.EmailHandler.sendEmail('welcome', opts, () => { + this.EmailSender.sendEmail.called.should.equal(true) + return done() + }) + }) + + it('should send lifecycle email if it is enabled', function(done) { + this.settings.email.lifecycle = true + this.EmailBuilder.buildEmail.returns({ type: 'lifecycle' }) + this.EmailSender.sendEmail.callsArgWith(1) + const opts = { to: 'bob@bob.com' } + return this.EmailHandler.sendEmail('welcome', opts, () => { + this.EmailSender.sendEmail.called.should.equal(true) + return done() + }) + }) + + return describe('with plain-text email content', function() { + beforeEach(function() { + return (this.text = 'hello there') + }) + + return it('should pass along the text field', function(done) { + this.EmailBuilder.buildEmail.returns({ + html: this.html, + text: this.text + }) + this.EmailSender.sendEmail.callsArgWith(1) + const opts = { to: 'bob@bob.com' } + return this.EmailHandler.sendEmail('welcome', opts, () => { + const args = this.EmailSender.sendEmail.args[0][0] + args.html.should.equal(this.html) + args.text.should.equal(this.text) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Email/EmailSenderTests.js b/services/web/test/unit/src/Email/EmailSenderTests.js new file mode 100644 index 0000000000..04bed5f4ca --- /dev/null +++ b/services/web/test/unit/src/Email/EmailSenderTests.js @@ -0,0 +1,170 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Email/EmailSender.js' +) +const { expect } = require('chai') + +describe('EmailSender', function() { + beforeEach(function() { + this.RateLimiter = { addCount: sinon.stub() } + + this.settings = { + email: { + transport: 'ses', + parameters: { + AWSAccessKeyID: 'key', + AWSSecretKey: 'secret' + }, + fromAddress: 'bob@bob.com', + replyToAddress: 'sally@gmail.com' + } + } + + this.sesClient = { sendMail: sinon.stub() } + + this.ses = { createTransport: () => this.sesClient } + + this.sender = SandboxedModule.require(modulePath, { + requires: { + nodemailer: this.ses, + 'nodemailer-mandrill-transport': {}, + 'nodemailer-sendgrid-transport': {}, + 'settings-sharelatex': this.settings, + '../../infrastructure/RateLimiter': this.RateLimiter, + 'logger-sharelatex': { + log() {}, + warn() {}, + err() {} + }, + 'metrics-sharelatex': { + inc() {} + } + } + }) + + return (this.opts = { + to: 'bob@bob.com', + subject: 'new email', + html: '' + }) + }) + + return describe('sendEmail', function() { + it('should set the properties on the email to send', function(done) { + this.sesClient.sendMail.callsArgWith(1) + + return this.sender.sendEmail(this.opts, err => { + expect(err).to.not.exist + const args = this.sesClient.sendMail.args[0][0] + args.html.should.equal(this.opts.html) + args.to.should.equal(this.opts.to) + args.subject.should.equal(this.opts.subject) + return done() + }) + }) + + it('should return a non-specific error', function(done) { + this.sesClient.sendMail.callsArgWith(1, 'error') + return this.sender.sendEmail({}, err => { + err.should.exist + err.toString().should.equal('Error: Cannot send email') + return done() + }) + }) + + it('should use the from address from settings', function(done) { + this.sesClient.sendMail.callsArgWith(1) + + return this.sender.sendEmail(this.opts, () => { + const args = this.sesClient.sendMail.args[0][0] + args.from.should.equal(this.settings.email.fromAddress) + return done() + }) + }) + + it('should use the reply to address from settings', function(done) { + this.sesClient.sendMail.callsArgWith(1) + + return this.sender.sendEmail(this.opts, () => { + const args = this.sesClient.sendMail.args[0][0] + args.replyTo.should.equal(this.settings.email.replyToAddress) + return done() + }) + }) + + it('should use the reply to address in options as an override', function(done) { + this.sesClient.sendMail.callsArgWith(1) + + this.opts.replyTo = 'someone@else.com' + return this.sender.sendEmail(this.opts, () => { + const args = this.sesClient.sendMail.args[0][0] + args.replyTo.should.equal(this.opts.replyTo) + return done() + }) + }) + + it('should not send an email when the rate limiter says no', function(done) { + this.opts.sendingUser_id = '12321312321' + this.RateLimiter.addCount.callsArgWith(1, null, false) + return this.sender.sendEmail(this.opts, () => { + this.sesClient.sendMail.called.should.equal(false) + return done() + }) + }) + + it('should send the email when the rate limtier says continue', function(done) { + this.sesClient.sendMail.callsArgWith(1) + this.opts.sendingUser_id = '12321312321' + this.RateLimiter.addCount.callsArgWith(1, null, true) + return this.sender.sendEmail(this.opts, () => { + this.sesClient.sendMail.called.should.equal(true) + return done() + }) + }) + + it('should not check the rate limiter when there is no sendingUser_id', function(done) { + this.sesClient.sendMail.callsArgWith(1) + return this.sender.sendEmail(this.opts, () => { + this.sesClient.sendMail.called.should.equal(true) + this.RateLimiter.addCount.called.should.equal(false) + return done() + }) + }) + + return describe('with plain-text email content', function() { + beforeEach(function() { + return (this.opts.text = 'hello there') + }) + + return it('should set the text property on the email to send', function(done) { + this.sesClient.sendMail.callsArgWith(1) + + return this.sender.sendEmail(this.opts, () => { + const args = this.sesClient.sendMail.args[0][0] + args.html.should.equal(this.opts.html) + args.text.should.equal(this.opts.text) + args.to.should.equal(this.opts.to) + args.subject.should.equal(this.opts.subject) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Email/SpamSafeTests.js b/services/web/test/unit/src/Email/SpamSafeTests.js new file mode 100644 index 0000000000..3273a93462 --- /dev/null +++ b/services/web/test/unit/src/Email/SpamSafeTests.js @@ -0,0 +1,86 @@ +/* eslint-disable + max-len, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Email/SpamSafe' +) +const SpamSafe = require(modulePath) +const { expect } = require('chai') + +describe('SpamSafe', () => + it('should reject spammy names', function() { + expect(SpamSafe.isSafeUserName('Charline Wałęsa')).to.equal(true) + expect( + SpamSafe.isSafeUserName( + "hey come buy this product im selling it's really good for you and it'll make your latex 10x guaranteed" + ) + ).to.equal(false) + expect(SpamSafe.isSafeUserName('隆太郎 宮本')).to.equal(true) + expect(SpamSafe.isSafeUserName('Visit haxx0red.com')).to.equal(false) + expect( + SpamSafe.isSafeUserName( + '加美汝VX:hihi661,金沙2001005com the first deposit will be _100%_' + ) + ).to.equal(false) + expect( + SpamSafe.isSafeProjectName( + 'Neural Networks: good for your health and will solve all your problems' + ) + ).to.equal(false) + expect( + SpamSafe.isSafeProjectName( + 'An analysis of the questions of the universe!' + ) + ).to.equal(true) + expect(SpamSafe.isSafeProjectName("A'p'o's't'r'o'p'h'e gallore")).to.equal( + true + ) + expect( + SpamSafe.isSafeProjectName( + 'come buy this => http://www.dopeproduct.com/search/?q=user123' + ) + ).to.equal(false) + expect( + SpamSafe.isSafeEmail('realistic-email+1@domain.sub-hyphen.com') + ).to.equal(true) + expect(SpamSafe.isSafeEmail('notquiteRight@evil$.com')).to.equal(false) + + expect(SpamSafe.safeUserName('Tammy Weinstįen', 'A User')).to.equal( + 'Tammy Weinstįen' + ) + expect(SpamSafe.safeUserName('haxx0red.com', 'A User')).to.equal('A User') + expect(SpamSafe.safeUserName('What$ Upp', 'A User')).to.equal('A User') + expect(SpamSafe.safeProjectName('Math-ematics!', 'A Project')).to.equal( + 'Math-ematics!' + ) + expect( + SpamSafe.safeProjectName( + `A Very long title for a very long book that will never be read${'a'.repeat( + 250 + )}`, + 'A Project' + ) + ).to.equal('A Project') + expect( + SpamSafe.safeEmail('safe-ëmail@domain.com', 'A collaborator') + ).to.equal('safe-ëmail@domain.com') + expect( + SpamSafe.safeEmail('Բարեւ@another.domain', 'A collaborator') + ).to.equal('Բարեւ@another.domain') + expect( + SpamSafe.safeEmail(`me+${'a'.repeat(40)}@googoole.con`, 'A collaborator') + ).to.equal('A collaborator') + return expect( + SpamSafe.safeEmail('sendME$$$@iAmAprince.com', 'A collaborator') + ).to.equal('A collaborator') + })) diff --git a/services/web/test/unit/src/Exports/ExportsControllerTests.js b/services/web/test/unit/src/Exports/ExportsControllerTests.js new file mode 100644 index 0000000000..11fbd6943c --- /dev/null +++ b/services/web/test/unit/src/Exports/ExportsControllerTests.js @@ -0,0 +1,182 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const chai = require('chai') +const { expect } = chai +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Exports/ExportsController.js' +) + +describe('ExportsController', function() { + const project_id = '123njdskj9jlk' + const user_id = '123nd3ijdks' + const brand_variation_id = 22 + const firstName = 'first' + const lastName = 'last' + const title = 'title' + const description = 'description' + const author = 'author' + const license = 'other' + const show_source = true + + beforeEach(function() { + this.handler = { getUserNotifications: sinon.stub().callsArgWith(1) } + this.req = { + params: { + project_id, + brand_variation_id + }, + body: { + firstName, + lastName + }, + session: { + user: { + _id: user_id + } + }, + i18n: { + translate() {} + } + } + this.res = { + json: sinon.stub(), + status: sinon.stub() + } + this.res.status.returns(this.res) + this.next = sinon.stub() + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.req.session.user._id) + } + return (this.controller = SandboxedModule.require(modulePath, { + requires: { + './ExportsHandler': this.handler, + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../Authentication/AuthenticationController': this + .AuthenticationController + } + })) + }) + + describe('without gallery fields', () => + it('should ask the handler to perform the export', function(done) { + this.handler.exportProject = sinon + .stub() + .yields(null, { iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id, + user_id, + brand_variation_id, + first_name: firstName, + last_name: lastName + } + return this.controller.exportProject(this.req, { + json: body => { + expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ export_v1_id: 897 }) + return done() + } + }) + })) + + describe('with gallery fields', function() { + beforeEach(function() { + this.req.body.title = title + this.req.body.description = description + this.req.body.author = author + this.req.body.license = license + return (this.req.body.showSource = true) + }) + + return it('should ask the handler to perform the export', function(done) { + this.handler.exportProject = sinon + .stub() + .yields(null, { iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id, + user_id, + brand_variation_id, + first_name: firstName, + last_name: lastName, + title, + description, + author, + license, + show_source + } + return this.controller.exportProject(this.req, { + json: body => { + expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ export_v1_id: 897 }) + return done() + } + }) + }) + }) + + describe('with an error return from v1 to forward to the publish modal', () => + it('should forward the response onward', function(done) { + this.error_json = { status: 422, message: 'nope' } + this.handler.exportProject = sinon + .stub() + .yields({ forwardResponse: this.error_json }) + this.controller.exportProject(this.req, this.res, this.next) + expect(this.res.json.args[0][0]).to.deep.equal(this.error_json) + expect(this.res.status.args[0][0]).to.equal(this.error_json.status) + return done() + })) + + return it('should ask the handler to return the status of an export', function(done) { + this.handler.fetchExport = sinon.stub().yields( + null, + `{ \ +\"id\":897, \ +\"status_summary\":\"completed\", \ +\"status_detail\":\"all done\", \ +\"partner_submission_id\":\"abc123\", \ +\"v2_user_email\":\"la@tex.com\", \ +\"v2_user_first_name\":\"Arthur\", \ +\"v2_user_last_name\":\"Author\", \ +\"title\":\"my project\", \ +\"token\":\"token\" \ +}` + ) + + this.req.params = { project_id, export_id: 897 } + return this.controller.exportStatus(this.req, { + json: body => { + expect(body).to.deep.equal({ + export_json: { + status_summary: 'completed', + status_detail: 'all done', + partner_submission_id: 'abc123', + v2_user_email: 'la@tex.com', + v2_user_first_name: 'Arthur', + v2_user_last_name: 'Author', + title: 'my project', + token: 'token' + } + }) + return done() + } + }) + }) +}) diff --git a/services/web/test/unit/src/Exports/ExportsHandlerTests.js b/services/web/test/unit/src/Exports/ExportsHandlerTests.js new file mode 100644 index 0000000000..7a764577b9 --- /dev/null +++ b/services/web/test/unit/src/Exports/ExportsHandlerTests.js @@ -0,0 +1,699 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Exports/ExportsHandler.js' +const SandboxedModule = require('sandboxed-module') + +describe('ExportsHandler', function() { + beforeEach(function() { + this.stubRequest = {} + this.request = { + defaults: () => { + return this.stubRequest + } + } + this.ExportsHandler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../Project/ProjectGetter': (this.ProjectGetter = {}), + '../Project/ProjectHistoryHandler': (this.ProjectHistoryHandler = {}), + '../Project/ProjectLocator': (this.ProjectLocator = {}), + '../Project/ProjectRootDocManager': (this.ProjectRootDocManager = {}), + '../User/UserGetter': (this.UserGetter = {}), + 'settings-sharelatex': (this.settings = {}), + request: this.request + } + }) + this.project_id = 'project-id-123' + this.project_history_id = 987 + this.user_id = 'user-id-456' + this.brand_variation_id = 789 + this.title = 'title' + this.description = 'description' + this.author = 'author' + this.license = 'other' + this.show_source = true + this.export_params = { + project_id: this.project_id, + brand_variation_id: this.brand_variation_id, + user_id: this.user_id, + title: this.title, + description: this.description, + author: this.author, + license: this.license, + show_source: this.show_source + } + return (this.callback = sinon.stub()) + }) + + describe('exportProject', function() { + beforeEach(function() { + this.export_data = { iAmAnExport: true } + this.response_body = { iAmAResponseBody: true } + this.ExportsHandler._buildExport = sinon + .stub() + .yields(null, this.export_data) + return (this.ExportsHandler._requestExport = sinon + .stub() + .yields(null, this.response_body)) + }) + + describe('when all goes well', function() { + beforeEach(function(done) { + return this.ExportsHandler.exportProject( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + it('should build the export', function() { + return this.ExportsHandler._buildExport + .calledWith(this.export_params) + .should.equal(true) + }) + + it('should request the export', function() { + return this.ExportsHandler._requestExport + .calledWith(this.export_data) + .should.equal(true) + }) + + return it('should return the export', function() { + return this.callback + .calledWith(null, this.export_data) + .should.equal(true) + }) + }) + + describe("when request can't be built", function() { + beforeEach(function(done) { + this.ExportsHandler._buildExport = sinon + .stub() + .yields(new Error('cannot export project without root doc')) + return this.ExportsHandler.exportProject( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + return it('should return an error', function() { + return (this.callback.args[0][0] instanceof Error).should.equal(true) + }) + }) + + return describe('when export request returns an error to forward to the user', function() { + beforeEach(function(done) { + this.error_json = { status: 422, message: 'nope' } + this.ExportsHandler._requestExport = sinon + .stub() + .yields(null, { forwardResponse: this.error_json }) + return this.ExportsHandler.exportProject( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + return it('should return success and the response to forward', function() { + ;(this.callback.args[0][0] instanceof Error).should.equal(false) + return this.callback.calledWith(null, { + forwardResponse: this.error_json + }) + }) + }) + }) + + describe('_buildExport', function() { + beforeEach(function(done) { + this.project = { + id: this.project_id, + rootDoc_id: 'doc1_id', + compiler: 'pdflatex', + imageName: 'mock-image-name', + overleaf: { + id: this.project_history_id, // for projects imported from v1 + history: { + id: this.project_history_id + } + } + } + this.user = { + id: this.user_id, + first_name: 'Arthur', + last_name: 'Author', + email: 'arthur.author@arthurauthoring.org', + overleaf: { + id: 876 + } + } + this.rootDocPath = 'main.tex' + this.historyVersion = 777 + this.ProjectGetter.getProject = sinon.stub().yields(null, this.project) + this.ProjectHistoryHandler.ensureHistoryExistsForProject = sinon + .stub() + .yields(null) + this.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'main.tex' }]) + this.ProjectRootDocManager.ensureRootDocumentIsValid = sinon + .stub() + .callsArgWith(1, null) + this.UserGetter.getUser = sinon.stub().yields(null, this.user) + this.ExportsHandler._requestVersion = sinon + .stub() + .yields(null, this.historyVersion) + return done() + }) + + describe('when all goes well', function() { + beforeEach(function(done) { + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + it('should ensure the project has history', function() { + return this.ProjectHistoryHandler.ensureHistoryExistsForProject.called.should.equal( + true + ) + }) + + it('should request the project history version', function() { + return this.ExportsHandler._requestVersion.called.should.equal(true) + }) + + return it('should return export data', function() { + const expected_export_data = { + project: { + id: this.project_id, + rootDocPath: this.rootDocPath, + historyId: this.project_history_id, + historyVersion: this.historyVersion, + v1ProjectId: this.project_history_id, + metadata: { + compiler: 'pdflatex', + imageName: 'mock-image-name', + title: this.title, + description: this.description, + author: this.author, + license: this.license, + showSource: this.show_source + } + }, + user: { + id: this.user_id, + firstName: this.user.first_name, + lastName: this.user.last_name, + email: this.user.email, + orcidId: null, + v1UserId: 876 + }, + destination: { + brandVariationId: this.brand_variation_id + }, + options: { + callbackUrl: null + } + } + return this.callback + .calledWith(null, expected_export_data) + .should.equal(true) + }) + }) + + describe('when we send replacement user first and last name', function() { + beforeEach(function(done) { + this.custom_first_name = 'FIRST' + this.custom_last_name = 'LAST' + this.export_params.first_name = this.custom_first_name + this.export_params.last_name = this.custom_last_name + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + return it('should send the data from the user input', function() { + const expected_export_data = { + project: { + id: this.project_id, + rootDocPath: this.rootDocPath, + historyId: this.project_history_id, + historyVersion: this.historyVersion, + v1ProjectId: this.project_history_id, + metadata: { + compiler: 'pdflatex', + imageName: 'mock-image-name', + title: this.title, + description: this.description, + author: this.author, + license: this.license, + showSource: this.show_source + } + }, + user: { + id: this.user_id, + firstName: this.custom_first_name, + lastName: this.custom_last_name, + email: this.user.email, + orcidId: null, + v1UserId: 876 + }, + destination: { + brandVariationId: this.brand_variation_id + }, + options: { + callbackUrl: null + } + } + return this.callback + .calledWith(null, expected_export_data) + .should.equal(true) + }) + }) + + describe('when project is not found', function() { + beforeEach(function(done) { + this.ProjectGetter.getProject = sinon + .stub() + .yields(new Error('project not found')) + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + return it('should return an error', function() { + return (this.callback.args[0][0] instanceof Error).should.equal(true) + }) + }) + + describe('when project has no root doc', () => + describe('when a root doc can be set automatically', function() { + beforeEach(function(done) { + this.project.rootDoc_id = null + this.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'other.tex' }]) + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + it('should set a root doc', function() { + return this.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + true + ) + }) + + return it('should return export data', function() { + const expected_export_data = { + project: { + id: this.project_id, + rootDocPath: 'other.tex', + historyId: this.project_history_id, + historyVersion: this.historyVersion, + v1ProjectId: this.project_history_id, + metadata: { + compiler: 'pdflatex', + imageName: 'mock-image-name', + title: this.title, + description: this.description, + author: this.author, + license: this.license, + showSource: this.show_source + } + }, + user: { + id: this.user_id, + firstName: this.user.first_name, + lastName: this.user.last_name, + email: this.user.email, + orcidId: null, + v1UserId: 876 + }, + destination: { + brandVariationId: this.brand_variation_id + }, + options: { + callbackUrl: null + } + } + return this.callback + .calledWith(null, expected_export_data) + .should.equal(true) + }) + })) + + describe('when project has an invalid root doc', function() { + describe('when a new root doc can be set automatically', function() { + beforeEach(function(done) { + this.fakeDoc_id = '1a2b3c4d5e6f' + this.project.rootDoc_id = this.fakeDoc_id + this.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'other.tex' }]) + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + it('should set a valid root doc', function() { + return this.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + true + ) + }) + + return it('should return export data', function() { + const expected_export_data = { + project: { + id: this.project_id, + rootDocPath: 'other.tex', + historyId: this.project_history_id, + historyVersion: this.historyVersion, + v1ProjectId: this.project_history_id, + metadata: { + compiler: 'pdflatex', + imageName: 'mock-image-name', + title: this.title, + description: this.description, + author: this.author, + license: this.license, + showSource: this.show_source + } + }, + user: { + id: this.user_id, + firstName: this.user.first_name, + lastName: this.user.last_name, + email: this.user.email, + orcidId: null, + v1UserId: 876 + }, + destination: { + brandVariationId: this.brand_variation_id + }, + options: { + callbackUrl: null + } + } + return this.callback + .calledWith(null, expected_export_data) + .should.equal(true) + }) + }) + + return describe('when no root doc can be identified', function() { + beforeEach(function(done) { + this.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, null]) + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + return it('should return an error', function() { + return (this.callback.args[0][0] instanceof Error).should.equal(true) + }) + }) + }) + + describe('when user is not found', function() { + beforeEach(function(done) { + this.UserGetter.getUser = sinon + .stub() + .yields(new Error('user not found')) + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + return it('should return an error', function() { + return (this.callback.args[0][0] instanceof Error).should.equal(true) + }) + }) + + return describe('when project history request fails', function() { + beforeEach(function(done) { + this.ExportsHandler._requestVersion = sinon + .stub() + .yields(new Error('project history call failed')) + return this.ExportsHandler._buildExport( + this.export_params, + (error, export_data) => { + this.callback(error, export_data) + return done() + } + ) + }) + + return it('should return an error', function() { + return (this.callback.args[0][0] instanceof Error).should.equal(true) + }) + }) + }) + + describe('_requestExport', function() { + beforeEach(function(done) { + this.settings.apis = { + v1: { + url: 'http://localhost:5000', + user: 'overleaf', + pass: 'pass' + } + } + this.export_data = { iAmAnExport: true } + this.export_id = 4096 + this.stubPost = sinon + .stub() + .yields(null, { statusCode: 200 }, { exportId: this.export_id }) + return done() + }) + + describe('when all goes well', function() { + beforeEach(function(done) { + this.stubRequest.post = this.stubPost + return this.ExportsHandler._requestExport( + this.export_data, + (error, export_v1_id) => { + this.callback(error, export_v1_id) + return done() + } + ) + }) + + it('should issue the request', function() { + return expect(this.stubPost.getCall(0).args[0]).to.deep.equal({ + url: this.settings.apis.v1.url + '/api/v1/sharelatex/exports', + auth: { + user: this.settings.apis.v1.user, + pass: this.settings.apis.v1.pass + }, + json: this.export_data + }) + }) + + return it('should return the v1 export id', function() { + return this.callback.calledWith(null, this.export_id).should.equal(true) + }) + }) + + describe('when the request fails', function() { + beforeEach(function(done) { + this.stubRequest.post = sinon + .stub() + .yields(new Error('export request failed')) + return this.ExportsHandler._requestExport( + this.export_data, + (error, export_v1_id) => { + this.callback(error, export_v1_id) + return done() + } + ) + }) + + return it('should return an error', function() { + return (this.callback.args[0][0] instanceof Error).should.equal(true) + }) + }) + + return describe('when the request returns an error response to forward', function() { + beforeEach(function(done) { + this.error_code = 422 + this.error_json = { status: this.error_code, message: 'nope' } + this.stubRequest.post = sinon + .stub() + .yields(null, { statusCode: this.error_code }, this.error_json) + return this.ExportsHandler._requestExport( + this.export_data, + (error, export_v1_id) => { + this.callback(error, export_v1_id) + return done() + } + ) + }) + + return it('should return success and the response to forward', function() { + ;(this.callback.args[0][0] instanceof Error).should.equal(false) + return this.callback.calledWith(null, { + forwardResponse: this.error_json + }) + }) + }) + }) + + describe('fetchExport', function() { + beforeEach(function(done) { + this.settings.apis = { + v1: { + url: 'http://localhost:5000', + user: 'overleaf', + pass: 'pass' + } + } + this.export_id = 897 + this.body = '{"id":897, "status_summary":"completed"}' + this.stubGet = sinon + .stub() + .yields(null, { statusCode: 200 }, { body: this.body }) + return done() + }) + + return describe('when all goes well', function() { + beforeEach(function(done) { + this.stubRequest.get = this.stubGet + return this.ExportsHandler.fetchExport( + this.export_id, + (error, body) => { + this.callback(error, body) + return done() + } + ) + }) + + it('should issue the request', function() { + return expect(this.stubGet.getCall(0).args[0]).to.deep.equal({ + url: + this.settings.apis.v1.url + + '/api/v1/sharelatex/exports/' + + this.export_id, + auth: { + user: this.settings.apis.v1.user, + pass: this.settings.apis.v1.pass + } + }) + }) + + return it('should return the v1 export id', function() { + return this.callback + .calledWith(null, { body: this.body }) + .should.equal(true) + }) + }) + }) + + return describe('fetchDownload', function() { + beforeEach(function(done) { + this.settings.apis = { + v1: { + url: 'http://localhost:5000', + user: 'overleaf', + pass: 'pass' + } + } + this.export_id = 897 + this.body = + 'https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee' + this.stubGet = sinon + .stub() + .yields(null, { statusCode: 200 }, { body: this.body }) + return done() + }) + + return describe('when all goes well', function() { + beforeEach(function(done) { + this.stubRequest.get = this.stubGet + return this.ExportsHandler.fetchDownload( + this.export_id, + 'zip', + (error, body) => { + this.callback(error, body) + return done() + } + ) + }) + + it('should issue the request', function() { + return expect(this.stubGet.getCall(0).args[0]).to.deep.equal({ + url: + this.settings.apis.v1.url + + '/api/v1/sharelatex/exports/' + + this.export_id + + '/zip_url', + auth: { + user: this.settings.apis.v1.user, + pass: this.settings.apis.v1.pass + } + }) + }) + + return it('should return the v1 export id', function() { + return this.callback + .calledWith(null, { body: this.body }) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/FileStore/FileStoreControllerTests.js b/services/web/test/unit/src/FileStore/FileStoreControllerTests.js new file mode 100644 index 0000000000..703ef23db2 --- /dev/null +++ b/services/web/test/unit/src/FileStore/FileStoreControllerTests.js @@ -0,0 +1,211 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { assert } = require('chai') +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/FileStore/FileStoreController.js' +const SandboxedModule = require('sandboxed-module') + +describe('FileStoreController', function() { + beforeEach(function() { + this.FileStoreHandler = { getFileStream: sinon.stub() } + this.ProjectLocator = { findElement: sinon.stub() } + this.controller = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }), + '../Project/ProjectLocator': this.ProjectLocator, + './FileStoreHandler': this.FileStoreHandler + } + }) + this.stream = {} + this.project_id = '2k3j1lk3j21lk3j' + this.file_id = '12321kklj1lk3jk12' + this.req = { + params: { + Project_id: this.project_id, + File_id: this.file_id + }, + query: 'query string here', + get(key) { + return undefined + } + } + this.res = { + setHeader: sinon.stub(), + setContentDisposition: sinon.stub() + } + return (this.file = { name: 'myfile.png' }) + }) + + return describe('getFile', function() { + beforeEach(function() { + this.FileStoreHandler.getFileStream.callsArgWith(3, null, this.stream) + return this.ProjectLocator.findElement.callsArgWith(1, null, this.file) + }) + + it('should call the file store handler with the project_id file_id and any query string', function(done) { + this.stream.pipe = des => { + this.FileStoreHandler.getFileStream + .calledWith( + this.req.params.Project_id, + this.req.params.File_id, + this.req.query + ) + .should.equal(true) + return done() + } + return this.controller.getFile(this.req, this.res) + }) + + it('should pipe to res', function(done) { + this.stream.pipe = des => { + des.should.equal(this.res) + return done() + } + return this.controller.getFile(this.req, this.res) + }) + + it('should get the file from the db', function(done) { + this.stream.pipe = des => { + const opts = { + project_id: this.project_id, + element_id: this.file_id, + type: 'file' + } + this.ProjectLocator.findElement.calledWith(opts).should.equal(true) + return done() + } + return this.controller.getFile(this.req, this.res) + }) + + it('should set the Content-Disposition header', function(done) { + this.stream.pipe = des => { + this.res.setContentDisposition + .calledWith('attachment', { filename: this.file.name }) + .should.equal(true) + return done() + } + return this.controller.getFile(this.req, this.res) + }) + + // Test behaviour around handling html files + ;['.html', '.htm', '.xhtml'].forEach(extension => + describe(`with a '${extension}' file extension`, function() { + beforeEach(function() { + this.file.name = `bad${extension}` + return (this.req.get = key => { + if (key === 'User-Agent') { + return 'A generic browser' + } + }) + }) + + describe('from a non-ios browser', () => + it('should not set Content-Type', function(done) { + this.stream.pipe = des => { + this.res.setHeader + .calledWith('Content-Type', 'text/plain') + .should.equal(false) + return done() + } + return this.controller.getFile(this.req, this.res) + })) + + describe('from an iPhone', function() { + beforeEach(function() { + return (this.req.get = key => { + if (key === 'User-Agent') { + return 'An iPhone browser' + } + }) + }) + + return it("should set Content-Type to 'text/plain'", function(done) { + this.stream.pipe = des => { + this.res.setHeader + .calledWith('Content-Type', 'text/plain') + .should.equal(true) + return done() + } + return this.controller.getFile(this.req, this.res) + }) + }) + + return describe('from an iPad', function() { + beforeEach(function() { + return (this.req.get = key => { + if (key === 'User-Agent') { + return 'An iPad browser' + } + }) + }) + + return it("should set Content-Type to 'text/plain'", function(done) { + this.stream.pipe = des => { + this.res.setHeader + .calledWith('Content-Type', 'text/plain') + .should.equal(true) + return done() + } + return this.controller.getFile(this.req, this.res) + }) + }) + }) + ) + + // None of these should trigger the iOS/html logic + return [ + 'x.html-is-rad', + 'html.pdf', + '.html-is-good-for-hidden-files', + 'somefile' + ].forEach(filename => + describe(`with filename as '${filename}'`, function() { + beforeEach(function() { + this.user_agent = 'A generic browser' + this.file.name = filename + return (this.req.get = key => { + if (key === 'User-Agent') { + return this.user_agent + } + }) + }) + + return ['iPhone', 'iPad', 'Firefox', 'Chrome'].forEach(browser => + describe(`downloaded from ${browser}`, function() { + beforeEach(function() { + return (this.user_agent = `Some ${browser} thing`) + }) + + return it('Should not set the Content-type', function(done) { + this.stream.pipe = des => { + this.res.setHeader + .calledWith('Content-Type', 'text/plain') + .should.equal(false) + return done() + } + return this.controller.getFile(this.req, this.res) + }) + }) + ) + }) + ) + }) +}) diff --git a/services/web/test/unit/src/FileStore/FileStoreHandlerTests.js b/services/web/test/unit/src/FileStore/FileStoreHandlerTests.js new file mode 100644 index 0000000000..7586571353 --- /dev/null +++ b/services/web/test/unit/src/FileStore/FileStoreHandlerTests.js @@ -0,0 +1,492 @@ +/* eslint-disable + handle-callback-err, + max-len, + mocha/no-identical-title, + no-return-assign, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { assert } = require('chai') +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/FileStore/FileStoreHandler.js' +const SandboxedModule = require('sandboxed-module') + +describe('FileStoreHandler', function() { + beforeEach(function() { + let File + this.fs = { + createReadStream: sinon.stub(), + lstat: sinon.stub().callsArgWith(1, null, { + isFile: () => true, + isDirectory() { + return false + } + }) + } + this.writeStream = { + my: 'writeStream', + on(type, cb) { + if (type === 'response') { + return cb({ statusCode: 200 }) + } + } + } + this.readStream = { my: 'readStream', on: sinon.stub() } + this.request = sinon.stub() + this.settings = { + apis: { filestore: { url: 'http//filestore.sharelatex.test' } } + } + this.hashValue = '0123456789' + this.FileModel = File = class File { + constructor(options) { + ;({ name: this.name, hash: this.hash } = options) + this._id = 'file_id_here' + this.rev = 0 + if (options.linkedFileData != null) { + this.linkedFileData = options.linkedFileData + } + } + } + this.handler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + request: this.request, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }), + './FileHashManager': (this.FileHashManager = { + computeHash: sinon.stub().callsArgWith(1, null, this.hashValue) + }), + // FIXME: need to stub File object here + '../../models/File': { + File: this.FileModel + }, + fs: this.fs + } + }) + this.file_args = { name: 'upload-filename' } + this.file_id = 'file_id_here' + this.project_id = '1312312312' + this.fsPath = 'uploads/myfile.eps' + return (this.handler._buildUrl = sinon + .stub() + .returns('http://filestore.stubbedBuilder.com')) + }) + + describe('uploadFileFromDisk', function() { + beforeEach(function() { + return this.request.returns(this.writeStream) + }) + + it('should create read stream', function(done) { + this.fs.createReadStream.returns({ + pipe() {}, + on(type, cb) { + if (type === 'open') { + return cb() + } + } + }) + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + () => { + this.fs.createReadStream.calledWith(this.fsPath).should.equal(true) + return done() + } + ) + }) + + it('should pipe the read stream to request', function(done) { + this.request.returns(this.writeStream) + this.fs.createReadStream.returns({ + on(type, cb) { + if (type === 'open') { + return cb() + } + }, + pipe: o => { + this.writeStream.should.equal(o) + return done() + } + }) + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + () => {} + ) + }) + + it('should pass the correct options to request', function(done) { + this.fs.createReadStream.returns({ + pipe() {}, + on(type, cb) { + if (type === 'open') { + return cb() + } + } + }) + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + () => { + this.request.args[0][0].method.should.equal('post') + this.request.args[0][0].uri.should.equal(this.handler._buildUrl()) + return done() + } + ) + }) + + it('builds the correct url', function(done) { + this.fs.createReadStream.returns({ + pipe() {}, + on(type, cb) { + if (type === 'open') { + return cb() + } + } + }) + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + () => { + this.handler._buildUrl + .calledWith(this.project_id, this.file_id) + .should.equal(true) + return done() + } + ) + }) + + it('should callback with the url and fileRef', function(done) { + this.fs.createReadStream.returns({ + pipe() {}, + on(type, cb) { + if (type === 'open') { + return cb() + } + } + }) + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + (err, url, fileRef) => { + expect(err).to.not.exist + expect(url).to.equal(this.handler._buildUrl()) + expect(fileRef._id).to.equal(this.file_id) + expect(fileRef.hash).to.equal(this.hashValue) + return done() + } + ) + }) + + describe('symlink', function() { + beforeEach(function() { + return (this.fs.lstat = sinon.stub().callsArgWith(1, null, { + isFile: () => false, + isDirectory() { + return false + } + })) + }) + + return it('should not read file if it is symlink', function(done) { + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + () => { + this.fs.createReadStream.called.should.equal(false) + return done() + } + ) + }) + }) + + describe('symlink', () => + it('should not read file stat returns nothing', function(done) { + this.fs.lstat = sinon.stub().callsArgWith(1, null, null) + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + () => { + this.fs.createReadStream.called.should.equal(false) + return done() + } + ) + })) + + return describe('when upload fails', function() { + beforeEach(function() { + return (this.writeStream.on = function(type, cb) { + if (type === 'response') { + return cb({ statusCode: 500 }) + } + }) + }) + + return it('should callback with an error', function(done) { + this.fs.createReadStream.callCount = 0 + this.fs.createReadStream.returns({ + pipe() {}, + on(type, cb) { + if (type === 'open') { + return cb() + } + } + }) + return this.handler.uploadFileFromDisk( + this.project_id, + this.file_args, + this.fsPath, + err => { + expect(err).to.exist + expect(err).to.be.instanceof(Error) + expect(this.fs.createReadStream.callCount).to.equal( + this.handler.RETRY_ATTEMPTS + ) + return done() + } + ) + }) + }) + }) + + describe('deleteFile', function() { + it('should send a delete request to filestore api', function(done) { + this.request.callsArgWith(1, null) + return this.handler.deleteFile(this.project_id, this.file_id, err => { + assert.equal(err, undefined) + this.request.args[0][0].method.should.equal('delete') + this.request.args[0][0].uri.should.equal(this.handler._buildUrl()) + return done() + }) + }) + + it('should return the error if there is one', function(done) { + const error = 'my error' + this.request.callsArgWith(1, error) + return this.handler.deleteFile(this.project_id, this.file_id, err => { + assert.equal(err, error) + return done() + }) + }) + + return it('builds the correct url', function(done) { + this.request.callsArgWith(1, null) + return this.handler.deleteFile(this.project_id, this.file_id, err => { + this.handler._buildUrl + .calledWith(this.project_id, this.file_id) + .should.equal(true) + return done() + }) + }) + }) + + describe('getFileStream', function() { + beforeEach(function() { + this.query = {} + return this.request.returns(this.readStream) + }) + + it('should get the stream with the correct params', function(done) { + return this.handler.getFileStream( + this.project_id, + this.file_id, + this.query, + (err, stream) => { + this.request.args[0][0].method.should.equal('get') + this.request.args[0][0].uri.should.equal(this.handler._buildUrl()) + return done() + } + ) + }) + + it('should get stream from request', function(done) { + return this.handler.getFileStream( + this.project_id, + this.file_id, + this.query, + (err, stream) => { + stream.should.equal(this.readStream) + return done() + } + ) + }) + + it('builds the correct url', function(done) { + return this.handler.getFileStream( + this.project_id, + this.file_id, + this.query, + (err, stream) => { + this.handler._buildUrl + .calledWith(this.project_id, this.file_id) + .should.equal(true) + return done() + } + ) + }) + + it('should add an error handler', function(done) { + return this.handler.getFileStream( + this.project_id, + this.file_id, + this.query, + (err, stream) => { + stream.on.calledWith('error').should.equal(true) + return done() + } + ) + }) + + return describe('when range is specified in query', function() { + beforeEach(function() { + return (this.query = { range: '0-10' }) + }) + + it('should add a range header', function(done) { + return this.handler.getFileStream( + this.project_id, + this.file_id, + this.query, + (err, stream) => { + this.request.callCount.should.equal(1) + const { headers } = this.request.firstCall.args[0] + expect(headers).to.have.keys('range') + expect(headers['range']).to.equal('bytes=0-10') + return done() + } + ) + }) + + return describe('when range is invalid', () => + ['0-', '-100', 'one-two', 'nonsense'].forEach(r => { + beforeEach(function() { + return (this.query = { range: `${r}` }) + }) + + return it(`should not add a range header for '${r}'`, function(done) { + return this.handler.getFileStream( + this.project_id, + this.file_id, + this.query, + (err, stream) => { + this.request.callCount.should.equal(1) + const { headers } = this.request.firstCall.args[0] + expect(headers).to.not.have.keys('range') + return done() + } + ) + }) + })) + }) + }) + + return describe('copyFile', function() { + beforeEach(function() { + this.newProject_id = 'new project' + return (this.newFile_id = 'new file id') + }) + + it('should post json', function(done) { + this.request.callsArgWith(1, null, { statusCode: 200 }) + + return this.handler.copyFile( + this.project_id, + this.file_id, + this.newProject_id, + this.newFile_id, + () => { + this.request.args[0][0].method.should.equal('put') + this.request.args[0][0].uri.should.equal(this.handler._buildUrl()) + this.request.args[0][0].json.source.project_id.should.equal( + this.project_id + ) + this.request.args[0][0].json.source.file_id.should.equal(this.file_id) + return done() + } + ) + }) + + it('builds the correct url', function(done) { + this.request.callsArgWith(1, null, { statusCode: 200 }) + return this.handler.copyFile( + this.project_id, + this.file_id, + this.newProject_id, + this.newFile_id, + () => { + this.handler._buildUrl + .calledWith(this.newProject_id, this.newFile_id) + .should.equal(true) + return done() + } + ) + }) + + it('returns the url', function(done) { + this.request.callsArgWith(1, null, { statusCode: 200 }) + return this.handler.copyFile( + this.project_id, + this.file_id, + this.newProject_id, + this.newFile_id, + (err, url) => { + url.should.equal('http://filestore.stubbedBuilder.com') + return done() + } + ) + }) + + it('should return the err', function(done) { + const error = 'errrror' + this.request.callsArgWith(1, error) + return this.handler.copyFile( + this.project_id, + this.file_id, + this.newProject_id, + this.newFile_id, + err => { + err.should.equal(error) + return done() + } + ) + }) + + return it('should return an error for a non-success statusCode', function(done) { + this.request.callsArgWith(1, null, { statusCode: 500 }) + return this.handler.copyFile( + this.project_id, + this.file_id, + this.newProject_id, + this.newFile_id, + err => { + err.should.be.an('error') + err.message.should.equal( + 'non-ok response from filestore for copyFile: 500' + ) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/History/HistoryControllerTests.js b/services/web/test/unit/src/History/HistoryControllerTests.js new file mode 100644 index 0000000000..c611605741 --- /dev/null +++ b/services/web/test/unit/src/History/HistoryControllerTests.js @@ -0,0 +1,369 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +chai.should() +const sinon = require('sinon') + +const Errors = require('../../../../app/src/Features/Errors/Errors') + +const modulePath = '../../../../app/src/Features/History/HistoryController' +const SandboxedModule = require('sandboxed-module') + +describe('HistoryController', function() { + beforeEach(function() { + this.callback = sinon.stub() + this.user_id = 'user-id-123' + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user_id) + } + this.HistoryController = SandboxedModule.require(modulePath, { + requires: { + request: (this.request = sinon.stub()), + 'settings-sharelatex': (this.settings = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../Errors/Errors': Errors, + './HistoryManager': (this.HistoryManager = {}), + '../Project/ProjectDetailsHandler': (this.ProjectDetailsHandler = {}), + '../Project/ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}), + './RestoreManager': (this.RestoreManager = {}) + } + }) + return (this.settings.apis = { + trackchanges: { + enabled: false, + url: 'http://trackchanges.example.com' + }, + project_history: { + url: 'http://project_history.example.com' + } + }) + }) + + describe('selectHistoryApi', function() { + beforeEach(function() { + this.req = { url: '/mock/url', method: 'POST' } + this.res = 'mock-res' + return (this.next = sinon.stub()) + }) + + describe('for a project with project history', function() { + beforeEach(function() { + this.ProjectDetailsHandler.getDetails = sinon + .stub() + .callsArgWith(1, null, { + overleaf: { history: { id: 42, display: true } } + }) + return this.HistoryController.selectHistoryApi( + this.req, + this.res, + this.next + ) + }) + + return it('should set the flag for project history to true', function() { + return this.req.useProjectHistory.should.equal(true) + }) + }) + + return describe('for any other project ', function() { + beforeEach(function() { + this.ProjectDetailsHandler.getDetails = sinon + .stub() + .callsArgWith(1, null, {}) + return this.HistoryController.selectHistoryApi( + this.req, + this.res, + this.next + ) + }) + + return it('should not set the flag for project history to false', function() { + return this.req.useProjectHistory.should.equal(false) + }) + }) + }) + + describe('proxyToHistoryApi', function() { + beforeEach(function() { + this.req = { url: '/mock/url', method: 'POST' } + this.res = 'mock-res' + this.next = sinon.stub() + this.proxy = { + events: {}, + pipe: sinon.stub(), + on(event, handler) { + return (this.events[event] = handler) + } + } + return this.request.returns(this.proxy) + }) + + describe('for a project with the project history flag', function() { + beforeEach(function() { + this.req.useProjectHistory = true + return this.HistoryController.proxyToHistoryApi( + this.req, + this.res, + this.next + ) + }) + + it('should get the user id', function() { + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should call the project history api', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.project_history.url}${this.req.url}`, + method: this.req.method, + headers: { + 'X-User-Id': this.user_id + } + }) + .should.equal(true) + }) + + return it('should pipe the response to the client', function() { + return this.proxy.pipe.calledWith(this.res).should.equal(true) + }) + }) + + describe('for a project without the project history flag', function() { + beforeEach(function() { + this.req.useProjectHistory = false + return this.HistoryController.proxyToHistoryApi( + this.req, + this.res, + this.next + ) + }) + + it('should get the user id', function() { + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should call the track changes api', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.trackchanges.url}${this.req.url}`, + method: this.req.method, + headers: { + 'X-User-Id': this.user_id + } + }) + .should.equal(true) + }) + + return it('should pipe the response to the client', function() { + return this.proxy.pipe.calledWith(this.res).should.equal(true) + }) + }) + + return describe('with an error', function() { + beforeEach(function() { + this.HistoryController.proxyToHistoryApi(this.req, this.res, this.next) + return this.proxy.events['error'].call( + this.proxy, + (this.error = new Error('oops')) + ) + }) + + return it('should pass the error up the call chain', function() { + return this.next.calledWith(this.error).should.equal(true) + }) + }) + }) + + describe('proxyToHistoryApiAndInjectUserDetails', function() { + beforeEach(function() { + this.req = { url: '/mock/url', method: 'POST' } + this.res = { json: sinon.stub() } + this.next = sinon.stub() + this.request.yields(null, { statusCode: 200 }, (this.data = 'mock-data')) + return (this.HistoryManager.injectUserDetails = sinon + .stub() + .yields(null, (this.data_with_users = 'mock-injected-data'))) + }) + + describe('for a project with the project history flag', function() { + beforeEach(function() { + this.req.useProjectHistory = true + return this.HistoryController.proxyToHistoryApiAndInjectUserDetails( + this.req, + this.res, + this.next + ) + }) + + it('should get the user id', function() { + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should call the project history api', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.project_history.url}${this.req.url}`, + method: this.req.method, + json: true, + headers: { + 'X-User-Id': this.user_id + } + }) + .should.equal(true) + }) + + it('should inject the user data', function() { + return this.HistoryManager.injectUserDetails + .calledWith(this.data) + .should.equal(true) + }) + + return it('should return the data with users to the client', function() { + return this.res.json.calledWith(this.data_with_users).should.equal(true) + }) + }) + + return describe('for a project without the project history flag', function() { + beforeEach(function() { + this.req.useProjectHistory = false + return this.HistoryController.proxyToHistoryApiAndInjectUserDetails( + this.req, + this.res, + this.next + ) + }) + + it('should get the user id', function() { + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should call the track changes api', function() { + return this.request + .calledWith({ + url: `${this.settings.apis.trackchanges.url}${this.req.url}`, + method: this.req.method, + json: true, + headers: { + 'X-User-Id': this.user_id + } + }) + .should.equal(true) + }) + + it('should inject the user data', function() { + return this.HistoryManager.injectUserDetails + .calledWith(this.data) + .should.equal(true) + }) + + return it('should return the data with users to the client', function() { + return this.res.json.calledWith(this.data_with_users).should.equal(true) + }) + }) + }) + + describe('proxyToHistoryApiAndInjectUserDetails (with the history API failing)', function() { + beforeEach(function() { + this.req = { url: '/mock/url', method: 'POST', useProjectHistory: true } + this.res = { json: sinon.stub() } + this.next = sinon.stub() + this.request.yields(null, { statusCode: 500 }, (this.data = 'mock-data')) + this.HistoryManager.injectUserDetails = sinon + .stub() + .yields(null, (this.data_with_users = 'mock-injected-data')) + return this.HistoryController.proxyToHistoryApiAndInjectUserDetails( + this.req, + this.res, + this.next + ) + }) + + it('should not inject the user data', function() { + return this.HistoryManager.injectUserDetails + .calledWith(this.data) + .should.equal(false) + }) + + return it('should not return the data with users to the client', function() { + return this.res.json.calledWith(this.data_with_users).should.equal(false) + }) + }) + + return describe('resyncProjectHistory', function() { + describe('for a project without project-history enabled', function() { + beforeEach(function() { + this.project_id = 'mock-project-id' + this.req = { params: { Project_id: this.project_id } } + this.res = { sendStatus: sinon.stub() } + this.next = sinon.stub() + + this.error = new Errors.ProjectHistoryDisabledError() + this.ProjectEntityUpdateHandler.resyncProjectHistory = sinon + .stub() + .yields(this.error) + + return this.HistoryController.resyncProjectHistory( + this.req, + this.res, + this.next + ) + }) + + return it('response with a 404', function() { + return this.res.sendStatus.calledWith(404).should.equal(true) + }) + }) + + return describe('for a project with project-history enabled', function() { + beforeEach(function() { + this.project_id = 'mock-project-id' + this.req = { params: { Project_id: this.project_id } } + this.res = { sendStatus: sinon.stub() } + this.next = sinon.stub() + + this.ProjectEntityUpdateHandler.resyncProjectHistory = sinon + .stub() + .yields() + + return this.HistoryController.resyncProjectHistory( + this.req, + this.res, + this.next + ) + }) + + it('resyncs the project', function() { + return this.ProjectEntityUpdateHandler.resyncProjectHistory + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('responds with a 204', function() { + return this.res.sendStatus.calledWith(204).should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/History/HistoryManagerTests.js b/services/web/test/unit/src/History/HistoryManagerTests.js new file mode 100644 index 0000000000..8a26d7a1dc --- /dev/null +++ b/services/web/test/unit/src/History/HistoryManagerTests.js @@ -0,0 +1,301 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/History/HistoryManager' +const SandboxedModule = require('sandboxed-module') + +describe('HistoryManager', function() { + beforeEach(function() { + this.callback = sinon.stub() + this.user_id = 'user-id-123' + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user_id) + } + this.HistoryManager = SandboxedModule.require(modulePath, { + requires: { + request: (this.request = sinon.stub()), + 'settings-sharelatex': (this.settings = {}), + '../User/UserGetter': (this.UserGetter = {}) + } + }) + return (this.settings.apis = { + trackchanges: { + enabled: false, + url: 'http://trackchanges.example.com' + }, + project_history: { + url: 'http://project_history.example.com' + } + }) + }) + + describe('initializeProject', function() { + describe('with project history enabled', function() { + beforeEach(function() { + return (this.settings.apis.project_history.initializeHistoryForNewProjects = true) + }) + + describe('project history returns a successful response', function() { + beforeEach(function() { + this.overleaf_id = 1234 + this.res = { statusCode: 200 } + this.body = JSON.stringify({ project: { id: this.overleaf_id } }) + this.request.post = sinon + .stub() + .callsArgWith(1, null, this.res, this.body) + + return this.HistoryManager.initializeProject(this.callback) + }) + + it('should call the project history api', function() { + return this.request.post + .calledWith({ + url: `${this.settings.apis.project_history.url}/project` + }) + .should.equal(true) + }) + + return it('should return the callback with the overleaf id', function() { + return this.callback + .calledWithExactly(null, { overleaf_id: this.overleaf_id }) + .should.equal(true) + }) + }) + + describe('project history returns a response without the project id', function() { + beforeEach(function() { + this.res = { statusCode: 200 } + this.body = JSON.stringify({ project: {} }) + this.request.post = sinon + .stub() + .callsArgWith(1, null, this.res, this.body) + + return this.HistoryManager.initializeProject(this.callback) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + sinon.match.has( + 'message', + 'project-history did not provide an id' + ) + ) + .should.equal(true) + }) + }) + + describe('project history returns a unsuccessful response', function() { + beforeEach(function() { + this.res = { statusCode: 404 } + this.request.post = sinon.stub().callsArgWith(1, null, this.res) + + return this.HistoryManager.initializeProject(this.callback) + }) + + return it('should return the callback with an error', function() { + return this.callback + .calledWith( + sinon.match.has( + 'message', + 'project-history returned a non-success status code: 404' + ) + ) + .should.equal(true) + }) + }) + + return describe('project history errors', function() { + beforeEach(function() { + this.error = sinon.stub() + this.request.post = sinon.stub().callsArgWith(1, this.error) + + return this.HistoryManager.initializeProject(this.callback) + }) + + return it('should return the callback with the error', function() { + return this.callback.calledWithExactly(this.error).should.equal(true) + }) + }) + }) + + return describe('with project history disabled', function() { + beforeEach(function() { + this.settings.apis.project_history.initializeHistoryForNewProjects = false + return this.HistoryManager.initializeProject(this.callback) + }) + + return it('should return the callback', function() { + return this.callback.calledWithExactly().should.equal(true) + }) + }) + }) + + return describe('injectUserDetails', function() { + beforeEach(function() { + this.user1 = { + _id: (this.user_id1 = '123456'), + first_name: 'Jane', + last_name: 'Doe', + email: 'jane@example.com' + } + this.user1_view = { + id: this.user_id1, + first_name: 'Jane', + last_name: 'Doe', + email: 'jane@example.com' + } + this.user2 = { + _id: (this.user_id2 = 'abcdef'), + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com' + } + this.user2_view = { + id: this.user_id2, + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com' + } + return (this.UserGetter.getUsers = sinon + .stub() + .yields(null, [this.user1, this.user2])) + }) + + describe('with a diff', function() { + it('should turn user_ids into user objects', function(done) { + return this.HistoryManager.injectUserDetails( + { + diff: [ + { + i: 'foo', + meta: { + users: [this.user_id1] + } + }, + { + i: 'bar', + meta: { + users: [this.user_id2] + } + } + ] + }, + (error, diff) => { + expect(error).to.be.null + expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) + expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) + return done() + } + ) + }) + + return it('should leave user objects', function(done) { + return this.HistoryManager.injectUserDetails( + { + diff: [ + { + i: 'foo', + meta: { + users: [this.user1_view] + } + }, + { + i: 'bar', + meta: { + users: [this.user_id2] + } + } + ] + }, + (error, diff) => { + expect(error).to.be.null + expect(diff.diff[0].meta.users).to.deep.equal([this.user1_view]) + expect(diff.diff[1].meta.users).to.deep.equal([this.user2_view]) + return done() + } + ) + }) + }) + + return describe('with a list of updates', function() { + it('should turn user_ids into user objects', function(done) { + return this.HistoryManager.injectUserDetails( + { + updates: [ + { + fromV: 5, + toV: 8, + meta: { + users: [this.user_id1] + } + }, + { + fromV: 4, + toV: 5, + meta: { + users: [this.user_id2] + } + } + ] + }, + (error, updates) => { + expect(error).to.be.null + expect(updates.updates[0].meta.users).to.deep.equal([ + this.user1_view + ]) + expect(updates.updates[1].meta.users).to.deep.equal([ + this.user2_view + ]) + return done() + } + ) + }) + + return it('should leave user objects', function(done) { + return this.HistoryManager.injectUserDetails( + { + updates: [ + { + fromV: 5, + toV: 8, + meta: { + users: [this.user1_view] + } + }, + { + fromV: 4, + toV: 5, + meta: { + users: [this.user_id2] + } + } + ] + }, + (error, updates) => { + expect(error).to.be.null + expect(updates.updates[0].meta.users).to.deep.equal([ + this.user1_view + ]) + expect(updates.updates[1].meta.users).to.deep.equal([ + this.user2_view + ]) + return done() + } + ) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/History/RestoreManagerTests.js b/services/web/test/unit/src/History/RestoreManagerTests.js new file mode 100644 index 0000000000..80bdf244d6 --- /dev/null +++ b/services/web/test/unit/src/History/RestoreManagerTests.js @@ -0,0 +1,218 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/History/RestoreManager' +) +const Errors = require('../../../../app/src/Features/Errors/Errors') +const tk = require('timekeeper') +const moment = require('moment') + +describe('RestoreManager', function() { + beforeEach(function() { + tk.freeze(Date.now()) // freeze the time for these tests + this.RestoreManager = SandboxedModule.require(modulePath, { + requires: { + '../../infrastructure/FileWriter': (this.FileWriter = {}), + '../Uploads/FileSystemImportManager': (this.FileSystemImportManager = {}), + '../Project/ProjectLocator': (this.ProjectLocator = {}), + '../Errors/Errors': Errors, + '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), + '../Editor/EditorController': (this.EditorController = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }) + } + }) + this.user_id = 'mock-user-id' + this.project_id = 'mock-project-id' + this.version = 42 + return (this.callback = sinon.stub()) + }) + + afterEach(() => tk.reset()) + + describe('restoreFileFromV2', function() { + beforeEach(function() { + this.RestoreManager._writeFileVersionToDisk = sinon + .stub() + .yields(null, (this.fsPath = '/tmp/path/on/disk')) + this.RestoreManager._findOrCreateFolder = sinon + .stub() + .yields(null, (this.folder_id = 'mock-folder-id')) + return (this.FileSystemImportManager.addEntity = sinon + .stub() + .yields(null, (this.entity = 'mock-entity'))) + }) + + describe('with a file not in a folder', function() { + beforeEach(function() { + this.pathname = 'foo.tex' + return this.RestoreManager.restoreFileFromV2( + this.user_id, + this.project_id, + this.version, + this.pathname, + this.callback + ) + }) + + it('should write the file version to disk', function() { + return this.RestoreManager._writeFileVersionToDisk + .calledWith(this.project_id, this.version, this.pathname) + .should.equal(true) + }) + + it('should find the root folder', function() { + return this.RestoreManager._findOrCreateFolder + .calledWith(this.project_id, '') + .should.equal(true) + }) + + it('should add the entity', function() { + return this.FileSystemImportManager.addEntity + .calledWith( + this.user_id, + this.project_id, + this.folder_id, + 'foo.tex', + this.fsPath, + false + ) + .should.equal(true) + }) + + return it('should call the callback with the entity', function() { + return this.callback.calledWith(null, this.entity).should.equal(true) + }) + }) + + return describe('with a file in a folder', function() { + beforeEach(function() { + this.pathname = 'foo/bar.tex' + return this.RestoreManager.restoreFileFromV2( + this.user_id, + this.project_id, + this.version, + this.pathname, + this.callback + ) + }) + + it('should find the folder', function() { + return this.RestoreManager._findOrCreateFolder + .calledWith(this.project_id, 'foo') + .should.equal(true) + }) + + return it('should add the entity by its basename', function() { + return this.FileSystemImportManager.addEntity + .calledWith( + this.user_id, + this.project_id, + this.folder_id, + 'bar.tex', + this.fsPath, + false + ) + .should.equal(true) + }) + }) + }) + + describe('_findOrCreateFolder', function() { + beforeEach(function() { + this.EditorController.mkdirp = sinon + .stub() + .yields(null, [], { _id: (this.folder_id = 'mock-folder-id') }) + return this.RestoreManager._findOrCreateFolder( + this.project_id, + 'folder/name', + this.callback + ) + }) + + it('should look up or create the folder', function() { + return this.EditorController.mkdirp + .calledWith(this.project_id, 'folder/name') + .should.equal(true) + }) + + return it('should return the folder_id', function() { + return this.callback.calledWith(null, this.folder_id).should.equal(true) + }) + }) + + return describe('_addEntityWithUniqueName', function() { + beforeEach(function() { + this.addEntityWithName = sinon.stub() + return (this.name = 'foo.tex') + }) + + describe('with a valid name', function() { + beforeEach(function() { + this.addEntityWithName.yields(null, (this.entity = 'mock-entity')) + return this.RestoreManager._addEntityWithUniqueName( + this.addEntityWithName, + this.name, + this.callback + ) + }) + + it('should add the entity', function() { + return this.addEntityWithName.calledWith(this.name).should.equal(true) + }) + + return it('should return the entity', function() { + return this.callback.calledWith(null, this.entity).should.equal(true) + }) + }) + + return describe('with an invalid name', function() { + beforeEach(function() { + this.addEntityWithName + .onFirstCall() + .yields(new Errors.InvalidNameError()) + this.addEntityWithName + .onSecondCall() + .yields(null, (this.entity = 'mock-entity')) + return this.RestoreManager._addEntityWithUniqueName( + this.addEntityWithName, + this.name, + this.callback + ) + }) + + it('should try to add the entity with its original name', function() { + return this.addEntityWithName.calledWith('foo.tex').should.equal(true) + }) + + it('should try to add the entity with a unique name', function() { + const date = moment(new Date()).format('Do MMM YY H:mm:ss') + return this.addEntityWithName + .calledWith(`foo (Restored on ${date}).tex`) + .should.equal(true) + }) + + return it('should return the entity', function() { + return this.callback.calledWith(null, this.entity).should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/InactiveData/InactiveProjectManagerTests.js b/services/web/test/unit/src/InactiveData/InactiveProjectManagerTests.js new file mode 100644 index 0000000000..c7a0e13b61 --- /dev/null +++ b/services/web/test/unit/src/InactiveData/InactiveProjectManagerTests.js @@ -0,0 +1,168 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/InactiveData/InactiveProjectManager' +) +const { expect } = require('chai') + +describe('InactiveProjectManager', function() { + beforeEach(function() { + this.settings = {} + this.DocstoreManager = { + unarchiveProject: sinon.stub(), + archiveProject: sinon.stub() + } + this.ProjectUpdateHandler = { + markAsActive: sinon.stub(), + markAsInactive: sinon.stub() + } + this.ProjectGetter = { getProject: sinon.stub() } + this.TrackChangesManager = { archiveProject: sinon.stub() } + this.InactiveProjectManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../Docstore/DocstoreManager': this.DocstoreManager, + '../Project/ProjectUpdateHandler': this.ProjectUpdateHandler, + '../Project/ProjectGetter': this.ProjectGetter, + '../TrackChanges/TrackChangesManager': this.TrackChangesManager, + '../../models/Project': {} + } + }) + return (this.project_id = '1234') + }) + + describe('reactivateProjectIfRequired', function() { + beforeEach(function() { + this.project = { active: false } + this.ProjectGetter.getProject.callsArgWith(2, null, this.project) + return this.ProjectUpdateHandler.markAsActive.callsArgWith(1) + }) + + it('should call unarchiveProject', function(done) { + this.DocstoreManager.unarchiveProject.callsArgWith(1) + return this.InactiveProjectManager.reactivateProjectIfRequired( + this.project_id, + err => { + this.DocstoreManager.unarchiveProject + .calledWith(this.project_id) + .should.equal(true) + this.ProjectUpdateHandler.markAsActive + .calledWith(this.project_id) + .should.equal(true) + return done() + } + ) + }) + + it('should not mark project as active if error with unarchinging', function(done) { + this.DocstoreManager.unarchiveProject.callsArgWith(1, 'error') + return this.InactiveProjectManager.reactivateProjectIfRequired( + this.project_id, + err => { + err.should.equal('error') + this.DocstoreManager.unarchiveProject + .calledWith(this.project_id) + .should.equal(true) + this.ProjectUpdateHandler.markAsActive + .calledWith(this.project_id) + .should.equal(false) + return done() + } + ) + }) + + return it('should not call unarchiveProject if it is active', function(done) { + this.project.active = true + this.DocstoreManager.unarchiveProject.callsArgWith(1) + return this.InactiveProjectManager.reactivateProjectIfRequired( + this.project_id, + err => { + this.DocstoreManager.unarchiveProject + .calledWith(this.project_id) + .should.equal(false) + this.ProjectUpdateHandler.markAsActive + .calledWith(this.project_id) + .should.equal(false) + return done() + } + ) + }) + }) + + return describe('deactivateProject', function() { + it('should call unarchiveProject and markAsInactive', function(done) { + this.DocstoreManager.archiveProject.callsArgWith(1) + this.TrackChangesManager.archiveProject.callsArgWith(1) + + this.ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + return this.InactiveProjectManager.deactivateProject( + this.project_id, + err => { + this.DocstoreManager.archiveProject + .calledWith(this.project_id) + .should.equal(true) + // @TrackChangesManager.archiveProject.calledWith(@project_id).should.equal true + this.ProjectUpdateHandler.markAsInactive + .calledWith(this.project_id) + .should.equal(true) + return done() + } + ) + }) + + return it('should not call markAsInactive if there was a problem archiving in docstore', function(done) { + this.DocstoreManager.archiveProject.callsArgWith(1, 'errorrr') + this.TrackChangesManager.archiveProject.callsArgWith(1) + + this.ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + return this.InactiveProjectManager.deactivateProject( + this.project_id, + err => { + err.should.equal('errorrr') + this.DocstoreManager.archiveProject + .calledWith(this.project_id) + .should.equal(true) + this.ProjectUpdateHandler.markAsInactive + .calledWith(this.project_id) + .should.equal(false) + return done() + } + ) + }) + }) +}) + +// it "should not call markAsInactive if there was a problem archiving in track changes", (done)-> +// @DocstoreManager.archiveProject.callsArgWith(1) +// @TrackChangesManager.archiveProject.callsArgWith(1, "errorrr") + +// @ProjectUpdateHandler.markAsInactive.callsArgWith(1) + +// @InactiveProjectManager.deactivateProject @project_id, (err)=> +// err.should.equal "errorrr" +// @DocstoreManager.archiveProject.calledWith(@project_id).should.equal true +// @ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal false +// done() diff --git a/services/web/test/unit/src/Institutions/InstitutionsAPITests.js b/services/web/test/unit/src/Institutions/InstitutionsAPITests.js new file mode 100644 index 0000000000..f146e55771 --- /dev/null +++ b/services/web/test/unit/src/Institutions/InstitutionsAPITests.js @@ -0,0 +1,334 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +let { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Institutions/InstitutionsAPI' +) +;({ expect } = require('chai')) + +describe('InstitutionsAPI', function() { + beforeEach(function() { + this.logger = { err: sinon.stub(), log() {} } + this.settings = { apis: { v1: { url: 'v1.url', user: '', pass: '' } } } + this.request = sinon.stub() + this.ipMatcherNotification = { + read: (this.markAsReadIpMatcher = sinon.stub().callsArgWith(1, null)) + } + this.InstitutionsAPI = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': this.logger, + 'metrics-sharelatex': { + timeAsyncMethod: sinon.stub() + }, + 'settings-sharelatex': this.settings, + request: this.request, + '../Notifications/NotificationsBuilder': { + ipMatcherAffiliation: sinon.stub().returns(this.ipMatcherNotification) + } + } + }) + + this.stubbedUser = { + _id: '3131231', + name: 'bob', + email: 'hello@world.com' + } + return (this.newEmail = 'bob@bob.com') + }) + + describe('getInstitutionAffiliations', function() { + it('get affiliations', function(done) { + this.institutionId = 123 + const responseBody = ['123abc', '456def'] + this.request.yields(null, { statusCode: 200 }, responseBody) + return this.InstitutionsAPI.getInstitutionAffiliations( + this.institutionId, + (err, body) => { + should.not.exist(err) + this.request.calledOnce.should.equal(true) + const requestOptions = this.request.lastCall.args[0] + const expectedUrl = `v1.url/api/v2/institutions/${ + this.institutionId + }/affiliations` + requestOptions.url.should.equal(expectedUrl) + requestOptions.method.should.equal('GET') + should.not.exist(requestOptions.body) + body.should.equal(responseBody) + return done() + } + ) + }) + + return it('handle empty response', function(done) { + this.settings.apis = null + return this.InstitutionsAPI.getInstitutionAffiliations( + this.institutionId, + (err, body) => { + should.not.exist(err) + expect(body).to.be.a('Array') + body.length.should.equal(0) + return done() + } + ) + }) + }) + + describe('getInstitutionLicences', () => + it('get licences', function(done) { + this.institutionId = 123 + const responseBody = { + lag: 'monthly', + data: [{ key: 'users', values: [{ x: '2018-01-01', y: 1 }] }] + } + this.request.yields(null, { statusCode: 200 }, responseBody) + const startDate = '1417392000' + const endDate = '1420848000' + return this.InstitutionsAPI.getInstitutionLicences( + this.institutionId, + startDate, + endDate, + 'monthly', + (err, body) => { + should.not.exist(err) + this.request.calledOnce.should.equal(true) + const requestOptions = this.request.lastCall.args[0] + const expectedUrl = `v1.url/api/v2/institutions/${ + this.institutionId + }/institution_licences` + requestOptions.url.should.equal(expectedUrl) + requestOptions.method.should.equal('GET') + requestOptions.body['start_date'].should.equal(startDate) + requestOptions.body['end_date'].should.equal(endDate) + requestOptions.body.lag.should.equal('monthly') + body.should.equal(responseBody) + return done() + } + ) + })) + + describe('getUserAffiliations', function() { + it('get affiliations', function(done) { + const responseBody = [{ foo: 'bar' }] + this.request.callsArgWith(1, null, { statusCode: 201 }, responseBody) + return this.InstitutionsAPI.getUserAffiliations( + this.stubbedUser._id, + (err, body) => { + should.not.exist(err) + this.request.calledOnce.should.equal(true) + const requestOptions = this.request.lastCall.args[0] + const expectedUrl = `v1.url/api/v2/users/${ + this.stubbedUser._id + }/affiliations` + requestOptions.url.should.equal(expectedUrl) + requestOptions.method.should.equal('GET') + should.not.exist(requestOptions.body) + body.should.equal(responseBody) + return done() + } + ) + }) + + it('handle error', function(done) { + const body = { errors: 'affiliation error message' } + this.request.callsArgWith(1, null, { statusCode: 503 }, body) + return this.InstitutionsAPI.getUserAffiliations( + this.stubbedUser._id, + err => { + should.exist(err) + err.message.should.have.string(503) + err.message.should.have.string(body.errors) + return done() + } + ) + }) + + return it('handle empty response', function(done) { + this.settings.apis = null + return this.InstitutionsAPI.getUserAffiliations( + this.stubbedUser._id, + (err, body) => { + should.not.exist(err) + expect(body).to.be.a('Array') + body.length.should.equal(0) + return done() + } + ) + }) + }) + + describe('addAffiliation', function() { + beforeEach(function() { + return this.request.callsArgWith(1, null, { statusCode: 201 }) + }) + + it('add affiliation', function(done) { + const affiliationOptions = { + university: { id: 1 }, + role: 'Prof', + department: 'Math', + confirmedAt: new Date() + } + return this.InstitutionsAPI.addAffiliation( + this.stubbedUser._id, + this.newEmail, + affiliationOptions, + err => { + should.not.exist(err) + this.request.calledOnce.should.equal(true) + const requestOptions = this.request.lastCall.args[0] + const expectedUrl = `v1.url/api/v2/users/${ + this.stubbedUser._id + }/affiliations` + requestOptions.url.should.equal(expectedUrl) + requestOptions.method.should.equal('POST') + + const { body } = requestOptions + Object.keys(body).length.should.equal(5) + body.email.should.equal(this.newEmail) + body.university.should.equal(affiliationOptions.university) + body.department.should.equal(affiliationOptions.department) + body.role.should.equal(affiliationOptions.role) + body.confirmedAt.should.equal(affiliationOptions.confirmedAt) + this.markAsReadIpMatcher.calledOnce.should.equal(true) + return done() + } + ) + }) + + return it('handle error', function(done) { + const body = { errors: 'affiliation error message' } + this.request.callsArgWith(1, null, { statusCode: 422 }, body) + return this.InstitutionsAPI.addAffiliation( + this.stubbedUser._id, + this.newEmail, + {}, + err => { + should.exist(err) + err.message.should.have.string(422) + err.message.should.have.string(body.errors) + return done() + } + ) + }) + }) + + describe('removeAffiliation', function() { + beforeEach(function() { + return this.request.callsArgWith(1, null, { statusCode: 404 }) + }) + + it('remove affiliation', function(done) { + return this.InstitutionsAPI.removeAffiliation( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.request.calledOnce.should.equal(true) + const requestOptions = this.request.lastCall.args[0] + const expectedUrl = `v1.url/api/v2/users/${ + this.stubbedUser._id + }/affiliations/remove` + requestOptions.url.should.equal(expectedUrl) + requestOptions.method.should.equal('POST') + expect(requestOptions.body).to.deep.equal({ email: this.newEmail }) + return done() + } + ) + }) + + return it('handle error', function(done) { + this.request.callsArgWith(1, null, { statusCode: 500 }) + return this.InstitutionsAPI.removeAffiliation( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + err.message.should.exist + return done() + } + ) + }) + }) + + describe('deleteAffiliations', function() { + it('delete affiliations', function(done) { + this.request.callsArgWith(1, null, { statusCode: 200 }) + return this.InstitutionsAPI.deleteAffiliations( + this.stubbedUser._id, + err => { + should.not.exist(err) + this.request.calledOnce.should.equal(true) + const requestOptions = this.request.lastCall.args[0] + const expectedUrl = `v1.url/api/v2/users/${ + this.stubbedUser._id + }/affiliations` + requestOptions.url.should.equal(expectedUrl) + requestOptions.method.should.equal('DELETE') + return done() + } + ) + }) + + return it('handle error', function(done) { + const body = { errors: 'affiliation error message' } + this.request.callsArgWith(1, null, { statusCode: 518 }, body) + return this.InstitutionsAPI.deleteAffiliations( + this.stubbedUser._id, + err => { + should.exist(err) + err.message.should.have.string(518) + err.message.should.have.string(body.errors) + return done() + } + ) + }) + }) + + return describe('endorseAffiliation', function() { + beforeEach(function() { + return this.request.callsArgWith(1, null, { statusCode: 204 }) + }) + + return it('endorse affiliation', function(done) { + return this.InstitutionsAPI.endorseAffiliation( + this.stubbedUser._id, + this.newEmail, + 'Student', + 'Physics', + err => { + should.not.exist(err) + this.request.calledOnce.should.equal(true) + const requestOptions = this.request.lastCall.args[0] + const expectedUrl = `v1.url/api/v2/users/${ + this.stubbedUser._id + }/affiliations/endorse` + requestOptions.url.should.equal(expectedUrl) + requestOptions.method.should.equal('POST') + + const { body } = requestOptions + Object.keys(body).length.should.equal(3) + body.email.should.equal(this.newEmail) + body.role.should.equal('Student') + body.department.should.equal('Physics') + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Institutions/InstitutionsControllerTests.js b/services/web/test/unit/src/Institutions/InstitutionsControllerTests.js new file mode 100644 index 0000000000..6c2f691d3b --- /dev/null +++ b/services/web/test/unit/src/Institutions/InstitutionsControllerTests.js @@ -0,0 +1,121 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Institutions/InstitutionsController' +) +const { expect } = require('chai') + +describe('InstitutionsController', function() { + beforeEach(function() { + this.logger = { err: sinon.stub(), log() {} } + this.host = 'mit.edu' + .split('') + .reverse() + .join('') + this.stubbedUser1 = { + _id: '3131231', + name: 'bob', + email: 'hello@world.com', + emails: [ + { email: 'stubb1@mit.edu', reversedHostname: this.host }, + { email: 'test@test.com', reversedHostname: 'test.com' }, + { email: 'another@mit.edu', reversedHostname: this.host } + ] + } + this.stubbedUser2 = { + _id: '3131232', + name: 'test', + email: 'hello2@world.com', + emails: [{ email: 'subb2@mit.edu', reversedHostname: this.host }] + } + + this.getUsersByHostname = sinon + .stub() + .callsArgWith(2, null, [this.stubbedUser1, this.stubbedUser2]) + this.addAffiliation = sinon.stub().callsArgWith(3, null) + this.refreshFeatures = sinon.stub().callsArgWith(2, null) + this.InstitutionsController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': this.logger, + '../User/UserGetter': { + getUsersByHostname: this.getUsersByHostname + }, + '../Institutions/InstitutionsAPI': { + addAffiliation: this.addAffiliation + }, + '../Subscription/FeaturesUpdater': { + refreshFeatures: this.refreshFeatures + } + } + }) + + this.req = { body: { hostname: 'mit.edu' } } + + this.res = { + send: sinon.stub(), + json: sinon.stub() + } + return (this.next = sinon.stub()) + }) + + return describe('affiliateUsers', function() { + it('should add affiliations for matching users', function(done) { + this.res.sendStatus = code => { + code.should.equal(200) + this.getUsersByHostname.calledOnce.should.equal(true) + this.addAffiliation.calledThrice.should.equal(true) + this.addAffiliation + .calledWith(this.stubbedUser1._id, this.stubbedUser1.emails[0].email) + .should.equal(true) + this.addAffiliation + .calledWith(this.stubbedUser1._id, this.stubbedUser1.emails[2].email) + .should.equal(true) + this.addAffiliation + .calledWith(this.stubbedUser2._id, this.stubbedUser2.emails[0].email) + .should.equal(true) + this.refreshFeatures + .calledWith(this.stubbedUser1._id, true) + .should.equal(true) + this.refreshFeatures + .calledWith(this.stubbedUser2._id, true) + .should.equal(true) + return done() + } + return this.InstitutionsController.confirmDomain( + this.req, + this.res, + this.next + ) + }) + + return it('should return errors if last affiliation cannot be added', function(done) { + this.addAffiliation.onCall(2).callsArgWith(3, new Error('error')) + this.next = error => { + expect(error).to.exist + this.getUsersByHostname.calledOnce.should.equal(true) + return done() + } + return this.InstitutionsController.confirmDomain( + this.req, + this.res, + this.next + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Institutions/InstitutionsFeaturesTests.js b/services/web/test/unit/src/Institutions/InstitutionsFeaturesTests.js new file mode 100644 index 0000000000..8e77b59e2a --- /dev/null +++ b/services/web/test/unit/src/Institutions/InstitutionsFeaturesTests.js @@ -0,0 +1,199 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Institutions/InstitutionsFeatures.js' +) + +describe('InstitutionsFeatures', function() { + beforeEach(function() { + this.InstitutionsGetter = { getConfirmedInstitutions: sinon.stub() } + this.PlansLocator = { findLocalPlanInSettings: sinon.stub() } + this.institutionPlanCode = 'institution_plan_code' + this.InstitutionsFeatures = SandboxedModule.require(modulePath, { + requires: { + './InstitutionsGetter': this.InstitutionsGetter, + '../Subscription/PlansLocator': this.PlansLocator, + 'settings-sharelatex': { + institutionPlanCode: this.institutionPlanCode + }, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + }) + + return (this.userId = '12345abcde') + }) + + describe('hasLicence', function() { + it('should handle error', function(done) { + this.InstitutionsGetter.getConfirmedInstitutions.yields(new Error('Nope')) + return this.InstitutionsFeatures.hasLicence(this.userId, function( + error, + hasLicence + ) { + expect(error).to.exist + return done() + }) + }) + + it('should return false if user has no confirmed affiliations', function(done) { + const institutions = [] + this.InstitutionsGetter.getConfirmedInstitutions.yields( + null, + institutions + ) + return this.InstitutionsFeatures.hasLicence(this.userId, function( + error, + hasLicence + ) { + expect(error).to.not.exist + expect(hasLicence).to.be.false + return done() + }) + }) + + it('should return false if user has no paid affiliations', function(done) { + const institutions = [{ licence: 'free' }] + this.InstitutionsGetter.getConfirmedInstitutions.yields( + null, + institutions + ) + return this.InstitutionsFeatures.hasLicence(this.userId, function( + error, + hasLicence + ) { + expect(error).to.not.exist + expect(hasLicence).to.be.false + return done() + }) + }) + + return it('should return true if user has confirmed paid affiliation', function(done) { + const institutions = [ + { licence: 'pro_plus' }, + { licence: 'free' }, + { licence: 'pro' }, + { licence: null } + ] + this.InstitutionsGetter.getConfirmedInstitutions.yields( + null, + institutions + ) + return this.InstitutionsFeatures.hasLicence(this.userId, function( + error, + hasLicence + ) { + expect(error).to.not.exist + expect(hasLicence).to.be.true + return done() + }) + }) + }) + + describe('getInstitutionsFeatures', function() { + beforeEach(function() { + this.InstitutionsFeatures.getInstitutionsPlan = sinon.stub() + this.testFeatures = { features: { institution: 'all' } } + return this.PlansLocator.findLocalPlanInSettings + .withArgs(this.institutionPlanCode) + .returns(this.testFeatures) + }) + + it('should handle error', function(done) { + this.InstitutionsFeatures.getInstitutionsPlan.yields(new Error('Nope')) + return this.InstitutionsFeatures.getInstitutionsFeatures( + this.userId, + function(error, features) { + expect(error).to.exist + return done() + } + ) + }) + + it('should return no feaures if user has no plan code', function(done) { + this.InstitutionsFeatures.getInstitutionsPlan.yields(null, null) + return this.InstitutionsFeatures.getInstitutionsFeatures( + this.userId, + function(error, features) { + expect(error).to.not.exist + expect(features).to.deep.equal({}) + return done() + } + ) + }) + + return it('should return feaures if user has affiliations plan code', function(done) { + this.InstitutionsFeatures.getInstitutionsPlan.yields( + null, + this.institutionPlanCode + ) + return this.InstitutionsFeatures.getInstitutionsFeatures( + this.userId, + (error, features) => { + expect(error).to.not.exist + expect(features).to.deep.equal(this.testFeatures.features) + return done() + } + ) + }) + }) + + return describe('getInstitutionsPlan', function() { + beforeEach(function() { + return (this.InstitutionsFeatures.hasLicence = sinon.stub()) + }) + + it('should handle error', function(done) { + this.InstitutionsFeatures.hasLicence.yields(new Error('Nope')) + return this.InstitutionsFeatures.getInstitutionsPlan( + this.userId, + function(error) { + expect(error).to.exist + return done() + } + ) + }) + + it('should return no plan if user has no licence', function(done) { + this.InstitutionsFeatures.hasLicence.yields(null, false) + return this.InstitutionsFeatures.getInstitutionsPlan( + this.userId, + function(error, plan) { + expect(error).to.not.exist + expect(plan).to.equal(null) + return done() + } + ) + }) + + return it('should return plan if user has licence', function(done) { + this.InstitutionsFeatures.hasLicence.yields(null, true) + return this.InstitutionsFeatures.getInstitutionsPlan( + this.userId, + (error, plan) => { + expect(error).to.not.exist + expect(plan).to.equal(this.institutionPlanCode) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Institutions/InstitutionsGetterTests.js b/services/web/test/unit/src/Institutions/InstitutionsGetterTests.js new file mode 100644 index 0000000000..2aa5530659 --- /dev/null +++ b/services/web/test/unit/src/Institutions/InstitutionsGetterTests.js @@ -0,0 +1,94 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Institutions/InstitutionsGetter.js' +) + +describe('InstitutionsGetter', function() { + beforeEach(function() { + this.UserGetter = { getUserFullEmails: sinon.stub() } + this.InstitutionsGetter = SandboxedModule.require(modulePath, { + requires: { + '../User/UserGetter': this.UserGetter, + '../UserMembership/UserMembershipsHandler': (this.UserMembershipsHandler = {}), + '../UserMembership/UserMembershipEntityConfigs': (this.UserMembershipEntityConfigs = {}), + 'logger-sharelatex': { + log() { + return console.log(arguments) + }, + err() {} + } + } + }) + + return (this.userId = '12345abcde') + }) + + return describe('getConfirmedInstitutions', function() { + it('filters unconfirmed affiliations', function(done) { + this.userEmails = [ + { + confirmedAt: null, + affiliation: { institution: { id: 123, confirmed: true } } + }, + { + confirmedAt: new Date(), + affiliation: { institution: { id: 456, confirmed: true } } + }, + { confirmedAt: new Date(), affiliation: null }, + { confirmedAt: new Date(), affiliation: { institution: null } }, + { + confirmedAt: new Date(), + affiliation: { institution: { id: 789, confirmed: false } } + } + ] + this.UserGetter.getUserFullEmails.yields(null, this.userEmails) + return this.InstitutionsGetter.getConfirmedInstitutions( + this.userId, + function(error, institutions) { + expect(error).to.not.exist + institutions.length.should.equal(1) + institutions[0].id.should.equal(456) + return done() + } + ) + }) + + it('should handle empty response', function(done) { + this.UserGetter.getUserFullEmails.yields(null, []) + return this.InstitutionsGetter.getConfirmedInstitutions( + this.userId, + function(error, institutions) { + expect(error).to.not.exist + institutions.length.should.equal(0) + return done() + } + ) + }) + + return it('should handle error', function(done) { + this.UserGetter.getUserFullEmails.yields(new Error('Nope')) + return this.InstitutionsGetter.getConfirmedInstitutions( + this.userId, + function(error, institutions) { + expect(error).to.exist + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Institutions/InstitutionsManagerTests.js b/services/web/test/unit/src/Institutions/InstitutionsManagerTests.js new file mode 100644 index 0000000000..59f96712b4 --- /dev/null +++ b/services/web/test/unit/src/Institutions/InstitutionsManagerTests.js @@ -0,0 +1,211 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Institutions/InstitutionsManager' +) +const { expect } = require('chai') + +describe('InstitutionsManager', function() { + beforeEach(function() { + this.institutionId = 123 + this.logger = { log() {} } + this.user = {} + this.getInstitutionAffiliations = sinon.stub() + this.refreshFeatures = sinon.stub().yields() + this.UserGetter = { + getUsersByAnyConfirmedEmail: sinon.stub().yields(), + getUser: sinon.stub().callsArgWith(1, null, this.user) + } + this.creator = { create: sinon.stub().callsArg(0) } + this.NotificationsBuilder = { + featuresUpgradedByAffiliation: sinon.stub().returns(this.creator), + redundantPersonalSubscription: sinon.stub().returns(this.creator) + } + this.SubscriptionLocator = { + getUsersSubscription: sinon.stub().callsArg(1) + } + this.institutionWithV1Data = { name: 'Wombat University' } + this.institution = { + fetchV1Data: sinon + .stub() + .callsArgWith(0, null, this.institutionWithV1Data) + } + this.InstitutionModel = { + Institution: { + findOne: sinon.stub().callsArgWith(1, null, this.institution) + } + } + this.subscriptionExec = sinon.stub().yields() + const SubscriptionModel = { + Subscription: { + find: () => { + return { + populate: () => { + return { exec: this.subscriptionExec } + } + } + } + } + } + this.Mongo = { ObjectId: sinon.stub().returnsArg(0) } + + return (this.InstitutionsManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': this.logger, + './InstitutionsAPI': { + getInstitutionAffiliations: this.getInstitutionAffiliations + }, + '../Subscription/FeaturesUpdater': { + refreshFeatures: this.refreshFeatures + }, + '../User/UserGetter': this.UserGetter, + '../Notifications/NotificationsBuilder': this.NotificationsBuilder, + '../Subscription/SubscriptionLocator': this.SubscriptionLocator, + '../../models/Institution': this.InstitutionModel, + '../../models/Subscription': SubscriptionModel, + '../../infrastructure/mongojs': this.Mongo + } + })) + }) + + describe('upgradeInstitutionUsers', function() { + beforeEach(function() { + this.user1Id = '123abc123abc123abc123abc' + this.user2Id = '456def456def456def456def' + this.affiliations = [{ user_id: this.user1Id }, { user_id: this.user2Id }] + this.user1 = { _id: this.user1Id } + this.user2 = { _id: this.user2Id } + this.subscription = { + planCode: 'pro', + groupPlan: false + } + this.UserGetter.getUser + .withArgs(this.user1Id) + .callsArgWith(1, null, this.user1) + this.UserGetter.getUser + .withArgs(this.user2Id) + .callsArgWith(1, null, this.user2) + this.SubscriptionLocator.getUsersSubscription + .withArgs(this.user2) + .callsArgWith(1, null, this.subscription) + this.refreshFeatures + .withArgs(this.user1Id) + .callsArgWith(2, null, {}, true) + return this.getInstitutionAffiliations.yields(null, this.affiliations) + }) + + it('refresh all users Features', function(done) { + return this.InstitutionsManager.upgradeInstitutionUsers( + this.institutionId, + error => { + should.not.exist(error) + sinon.assert.calledTwice(this.refreshFeatures) + return done() + } + ) + }) + + it('notifies users if their features have been upgraded', function(done) { + return this.InstitutionsManager.upgradeInstitutionUsers( + this.institutionId, + error => { + should.not.exist(error) + sinon.assert.calledOnce( + this.NotificationsBuilder.featuresUpgradedByAffiliation + ) + sinon.assert.calledWith( + this.NotificationsBuilder.featuresUpgradedByAffiliation, + this.affiliations[0], + this.user1 + ) + return done() + } + ) + }) + + return it('notifies users if they have a subscription that should be cancelled', function(done) { + return this.InstitutionsManager.upgradeInstitutionUsers( + this.institutionId, + error => { + should.not.exist(error) + sinon.assert.calledOnce( + this.NotificationsBuilder.redundantPersonalSubscription + ) + sinon.assert.calledWith( + this.NotificationsBuilder.redundantPersonalSubscription, + this.affiliations[1], + this.user2 + ) + return done() + } + ) + }) + }) + + describe('checkInstitutionUsers', () => + it('check all users Features', function(done) { + const affiliations = [{ email: 'foo@bar.com' }, { email: 'baz@boo.edu' }] + const stubbedUsers = [ + { + _id: '123abc123abc123abc123abc', + features: { collaborators: -1, trackChanges: true } + }, + { + _id: '456def456def456def456def', + features: { collaborators: 10, trackChanges: false } + }, + { + _id: '789def789def789def789def', + features: { collaborators: -1, trackChanges: false } + } + ] + this.getInstitutionAffiliations.yields(null, affiliations) + this.UserGetter.getUsersByAnyConfirmedEmail.yields(null, stubbedUsers) + return this.InstitutionsManager.checkInstitutionUsers( + this.institutionId, + (error, usersSummary) => { + should.not.exist(error) + usersSummary.totalConfirmedUsers.should.equal(3) + usersSummary.totalConfirmedProUsers.should.equal(1) + usersSummary.totalConfirmedNonProUsers.should.equal(2) + expect(usersSummary.confirmedNonProUsers).to.deep.equal([ + '456def456def456def456def', + '789def789def789def789def' + ]) + return done() + } + ) + })) + + return describe('getInstitutionUsersSubscriptions', () => + it('returns all institution users subscriptions', function(done) { + const stubbedUsers = [ + { user_id: '123abc123abc123abc123abc' }, + { user_id: '456def456def456def456def' }, + { user_id: '789def789def789def789def' } + ] + this.getInstitutionAffiliations.yields(null, stubbedUsers) + return this.InstitutionsManager.getInstitutionUsersSubscriptions( + this.institutionId, + (error, subscriptions) => { + should.not.exist(error) + sinon.assert.calledOnce(this.subscriptionExec) + return done() + } + ) + })) +}) diff --git a/services/web/test/unit/src/Metadata/MetaControllerTests.js b/services/web/test/unit/src/Metadata/MetaControllerTests.js new file mode 100644 index 0000000000..f70e88a65b --- /dev/null +++ b/services/web/test/unit/src/Metadata/MetaControllerTests.js @@ -0,0 +1,204 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Metadata/MetaController' +const SandboxedModule = require('sandboxed-module') + +describe('MetaController', function() { + beforeEach(function() { + this.projectId = 'somekindofid' + this.EditorRealTimeController = { + emitToRoom: sinon.stub() + } + this.MetaHandler = { + getAllMetaForProject: sinon.stub(), + getMetaForDoc: sinon.stub() + } + return (this.MetadataController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, + '../Editor/EditorRealTimeController': this.EditorRealTimeController, + './MetaHandler': this.MetaHandler + } + })) + }) + + describe('getMetadata', function() { + beforeEach(function() { + this.fakeLabels = { somedoc: ['a_label'] } + this.MetaHandler.getAllMetaForProject = sinon + .stub() + .callsArgWith(1, null, this.fakeLabels) + this.req = { params: { project_id: this.projectId } } + this.res = { json: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should call MetaHandler.getAllMetaForProject', function() { + this.MetadataController.getMetadata(this.req, this.res, this.next) + this.MetaHandler.getAllMetaForProject.callCount.should.equal(1) + return this.MetaHandler.getAllMetaForProject + .calledWith(this.projectId) + .should.equal(true) + }) + + it('should call not call next with an error', function() { + this.MetadataController.getMetadata(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + it('should send a json response', function() { + this.MetadataController.getMetadata(this.req, this.res, this.next) + this.res.json.callCount.should.equal(1) + return expect(this.res.json.lastCall.args[0]).to.have.all.keys([ + 'projectId', + 'projectMeta' + ]) + }) + + return describe('when MetaHandler.getAllMetaForProject produces an error', function() { + beforeEach(function() { + this.MetaHandler.getAllMetaForProject = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.req = { params: { project_id: this.projectId } } + this.res = { json: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should call MetaHandler.getAllMetaForProject', function() { + this.MetadataController.getMetadata(this.req, this.res, this.next) + this.MetaHandler.getAllMetaForProject.callCount.should.equal(1) + return this.MetaHandler.getAllMetaForProject + .calledWith(this.projectId) + .should.equal(true) + }) + + it('should call next with an error', function() { + this.MetadataController.getMetadata(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + return it('should not send a json response', function() { + this.MetadataController.getMetadata(this.req, this.res, this.next) + return this.res.json.callCount.should.equal(0) + }) + }) + }) + + return describe('broadcastMetadataForDoc', function() { + beforeEach(function() { + this.MetaHandler.getMetaForDoc = sinon + .stub() + .callsArgWith(2, null, this.fakeLabels) + this.EditorRealTimeController.emitToRoom = sinon.stub() + this.docId = 'somedoc' + this.req = { params: { project_id: this.projectId, doc_id: this.docId } } + this.res = { sendStatus: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should call MetaHandler.getMetaForDoc', function() { + this.MetadataController.broadcastMetadataForDoc( + this.req, + this.res, + this.next + ) + this.MetaHandler.getMetaForDoc.callCount.should.equal(1) + return this.MetaHandler.getMetaForDoc + .calledWith(this.projectId) + .should.equal(true) + }) + + it('should call not call next with an error', function() { + this.MetadataController.broadcastMetadataForDoc( + this.req, + this.res, + this.next + ) + return this.next.callCount.should.equal(0) + }) + + it('should send a success response', function() { + this.MetadataController.broadcastMetadataForDoc( + this.req, + this.res, + this.next + ) + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(200).should.equal(true) + }) + + it('should emit a message to room', function() { + this.MetadataController.broadcastMetadataForDoc( + this.req, + this.res, + this.next + ) + this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + const { lastCall } = this.EditorRealTimeController.emitToRoom + expect(lastCall.args[0]).to.equal(this.projectId) + expect(lastCall.args[1]).to.equal('broadcastDocMeta') + return expect(lastCall.args[2]).to.have.all.keys(['docId', 'meta']) + }) + + return describe('when MetaHandler.getMetaForDoc produces an error', function() { + beforeEach(function() { + this.MetaHandler.getMetaForDoc = sinon + .stub() + .callsArgWith(2, new Error('woops')) + this.EditorRealTimeController.emitToRoom = sinon.stub() + this.docId = 'somedoc' + this.req = { + params: { project_id: this.projectId, doc_id: this.docId } + } + this.res = { json: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should call MetaHandler.getMetaForDoc', function() { + this.MetadataController.broadcastMetadataForDoc( + this.req, + this.res, + this.next + ) + this.MetaHandler.getMetaForDoc.callCount.should.equal(1) + return this.MetaHandler.getMetaForDoc + .calledWith(this.projectId) + .should.equal(true) + }) + + it('should call next with an error', function() { + this.MetadataController.broadcastMetadataForDoc( + this.req, + this.res, + this.next + ) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + return it('should not send a json response', function() { + this.MetadataController.broadcastMetadataForDoc( + this.req, + this.res, + this.next + ) + return this.res.json.callCount.should.equal(0) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Metadata/MetaHandlerTests.js b/services/web/test/unit/src/Metadata/MetaHandlerTests.js new file mode 100644 index 0000000000..3db28a26d0 --- /dev/null +++ b/services/web/test/unit/src/Metadata/MetaHandlerTests.js @@ -0,0 +1,330 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Metadata/MetaHandler' +const SandboxedModule = require('sandboxed-module') + +describe('MetaHandler', function() { + beforeEach(function() { + this.projectId = 'someprojectid' + this.docId = 'somedocid' + this.ProjectEntityHandler = { + getAllDocs: sinon.stub(), + getDoc: sinon.stub() + } + this.DocumentUpdaterHandler = { + flushDocToMongo: sinon.stub() + } + this.packageMapping = { + foo: [ + { + caption: '\\bar', + snippet: '\\bar', + meta: 'foo-cmd', + score: 12 + }, + { + caption: '\\bat[]{}', + snippet: '\\bar[$1]{$2}', + meta: 'foo-cmd', + score: 10 + } + ], + baz: [ + { + caption: '\\longercommandtest{}', + snippet: '\\longercommandtest{$1}', + meta: 'baz-cmd', + score: 50 + } + ] + } + + return (this.MetaHandler = SandboxedModule.require(modulePath, { + requires: { + '../Project/ProjectEntityHandler': this.ProjectEntityHandler, + '../DocumentUpdater/DocumentUpdaterHandler': this + .DocumentUpdaterHandler, + './packageMapping': this.packageMapping + } + })) + }) + + describe('extractMetaFromDoc', function() { + beforeEach(function() { + return (this.lines = [ + '\\usepackage{foo}', + '\\usepackage{amsmath, booktabs}', + 'one', + 'two', + 'three \\label{aaa}', + 'four five', + '\\label{bbb}', + 'six seven' + ]) + }) + + return it('should extract all the labels and packages', function() { + const docMeta = this.MetaHandler.extractMetaFromDoc(this.lines) + return expect(docMeta).to.deep.equal({ + labels: ['aaa', 'bbb'], + packages: { + foo: [ + { + caption: '\\bar', + snippet: '\\bar', + meta: 'foo-cmd', + score: 12 + }, + { + caption: '\\bat[]{}', + snippet: '\\bar[$1]{$2}', + meta: 'foo-cmd', + score: 10 + } + ] + } + }) + }) + }) + + describe('extractMetaFromProjectDocs', function() { + beforeEach(function() { + return (this.docs = { + doc_one: { + _id: 'id_one', + lines: ['one', '\\label{aaa} two', 'three'] + }, + doc_two: { + _id: 'id_two', + lines: ['four'] + }, + doc_three: { + _id: 'id_three', + lines: ['\\label{bbb}', 'five six', 'seven eight \\label{ccc} nine'] + }, + doc_four: { + _id: 'id_four', + lines: [ + '\\usepackage[width=\\textwidth]{baz}', + '\\usepackage{amsmath}' + ] + }, + doc_five: { + _id: 'id_five', + lines: [ + '\\usepackage{foo,baz}', + '\\usepackage[options=foo]{hello}', + 'some text', + '\\section{this}\\label{sec:intro}', + 'In Section \\ref{sec:intro} we saw', + 'nothing' + ] + } + }) + }) + + return it('should extract all metadata', function() { + const projectMeta = this.MetaHandler.extractMetaFromProjectDocs(this.docs) + return expect(projectMeta).to.deep.equal({ + id_one: { labels: ['aaa'], packages: {} }, + id_two: { labels: [], packages: {} }, + id_three: { labels: ['bbb', 'ccc'], packages: {} }, + id_four: { + labels: [], + packages: { + baz: [ + { + caption: '\\longercommandtest{}', + snippet: '\\longercommandtest{$1}', + meta: 'baz-cmd', + score: 50 + } + ] + } + }, + id_five: { + labels: ['sec:intro'], + packages: { + foo: [ + { + caption: '\\bar', + snippet: '\\bar', + meta: 'foo-cmd', + score: 12 + }, + { + caption: '\\bat[]{}', + snippet: '\\bar[$1]{$2}', + meta: 'foo-cmd', + score: 10 + } + ], + baz: [ + { + caption: '\\longercommandtest{}', + snippet: '\\longercommandtest{$1}', + meta: 'baz-cmd', + score: 50 + } + ] + } + } + }) + }) + }) + + describe('getMetaForDoc', function() { + beforeEach(function() { + this.fakeLines = ['\\usepackage{abc}', 'one', '\\label{aaa}', 'two'] + this.fakeMeta = { labels: ['aaa'], packages: ['abc'] } + this.DocumentUpdaterHandler.flushDocToMongo = sinon + .stub() + .callsArgWith(2, null) + this.ProjectEntityHandler.getDoc = sinon + .stub() + .callsArgWith(2, null, this.fakeLines) + this.MetaHandler.extractMetaFromDoc = sinon.stub().returns(this.fakeMeta) + return (this.call = callback => { + return this.MetaHandler.getMetaForDoc( + this.projectId, + this.docId, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, docMeta) => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should produce docMeta', function(done) { + return this.call((err, docMeta) => { + expect(docMeta).to.equal(this.fakeMeta) + return done() + }) + }) + + it('should call flushDocToMongo', function(done) { + return this.call((err, docMeta) => { + this.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(1) + this.DocumentUpdaterHandler.flushDocToMongo + .calledWith(this.projectId, this.docId) + .should.equal(true) + return done() + }) + }) + + it('should call getDoc', function(done) { + return this.call((err, docMeta) => { + this.ProjectEntityHandler.getDoc.callCount.should.equal(1) + this.ProjectEntityHandler.getDoc + .calledWith(this.projectId, this.docId) + .should.equal(true) + return done() + }) + }) + + return it('should call extractMetaFromDoc', function(done) { + return this.call((err, docMeta) => { + this.MetaHandler.extractMetaFromDoc.callCount.should.equal(1) + this.MetaHandler.extractMetaFromDoc + .calledWith(this.fakeLines) + .should.equal(true) + return done() + }) + }) + }) + + return describe('getAllMetaForProject', function() { + beforeEach(function() { + this.fakeDocs = { + doc_one: { + lines: ['\\usepackage[some-options,more=foo]{foo}', '\\label{aaa}'] + } + } + + this.fakeMeta = { + labels: ['aaa'], + packages: { + foo: [ + { + caption: '\\bar', + snippet: '\\bar', + meta: 'foo-cmd', + score: 12 + }, + { + caption: '\\bat[]{}', + snippet: '\\bar[$1]{$2}', + meta: 'foo-cmd', + score: 10 + } + ] + } + } + this.DocumentUpdaterHandler.flushProjectToMongo = sinon + .stub() + .callsArgWith(1, null) + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.fakeDocs) + this.MetaHandler.extractMetaFromProjectDocs = sinon + .stub() + .returns(this.fakeMeta) + return (this.call = callback => { + return this.MetaHandler.getAllMetaForProject(this.projectId, callback) + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, projectMeta) => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should produce projectMeta', function(done) { + return this.call((err, projectMeta) => { + expect(projectMeta).to.equal(this.fakeMeta) + return done() + }) + }) + + it('should call getAllDocs', function(done) { + return this.call((err, projectMeta) => { + this.ProjectEntityHandler.getAllDocs.callCount.should.equal(1) + this.ProjectEntityHandler.getAllDocs + .calledWith(this.projectId) + .should.equal(true) + return done() + }) + }) + + return it('should call extractMetaFromDoc', function(done) { + return this.call((err, docMeta) => { + this.MetaHandler.extractMetaFromProjectDocs.callCount.should.equal(1) + this.MetaHandler.extractMetaFromProjectDocs + .calledWith(this.fakeDocs) + .should.equal(true) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Notifications/NotificationsBuilderTests.js b/services/web/test/unit/src/Notifications/NotificationsBuilderTests.js new file mode 100644 index 0000000000..92fb45fde1 --- /dev/null +++ b/services/web/test/unit/src/Notifications/NotificationsBuilderTests.js @@ -0,0 +1,70 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const { assert } = require('chai') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Notifications/NotificationsBuilder.js' +) + +describe('NotificationsBuilder', function() { + const user_id = '123nd3ijdks' + + beforeEach(function() { + this.handler = { createNotification: sinon.stub().callsArgWith(6) } + + this.settings = { apis: { v1: { url: 'v1.url', user: '', pass: '' } } } + this.body = { id: 1, name: 'stanford', enrolment_ad_html: 'v1 ad content' } + const response = { statusCode: 200 } + this.request = sinon + .stub() + .returns(this.stubResponse) + .callsArgWith(1, null, response, this.body) + return (this.controller = SandboxedModule.require(modulePath, { + requires: { + './NotificationsHandler': this.handler, + 'settings-sharelatex': this.settings, + request: this.request, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + return it('should call v1 and create affiliation notifications', function(done) { + const ip = '192.168.0.1' + return this.controller + .ipMatcherAffiliation(user_id) + .create(ip, callback => { + this.request.calledOnce.should.equal(true) + const expectedOpts = { + university_name: this.body.name, + content: this.body.enrolment_ad_html + } + this.handler.createNotification + .calledWith( + user_id, + `ip-matched-affiliation-${this.body.id}`, + 'notification_ip_matched_affiliation', + expectedOpts + ) + .should.equal(true) + return done() + }) + }) +}) diff --git a/services/web/test/unit/src/Notifications/NotificationsControllerTests.js b/services/web/test/unit/src/Notifications/NotificationsControllerTests.js new file mode 100644 index 0000000000..a289a54814 --- /dev/null +++ b/services/web/test/unit/src/Notifications/NotificationsControllerTests.js @@ -0,0 +1,90 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Notifications/NotificationsController.js' +) + +describe('NotificationsController', function() { + const user_id = '123nd3ijdks' + const notification_id = '123njdskj9jlk' + + beforeEach(function() { + this.handler = { + getUserNotifications: sinon.stub().callsArgWith(1), + markAsRead: sinon.stub().callsArgWith(2) + } + this.req = { + params: { + notification_id + }, + session: { + user: { + _id: user_id + } + }, + i18n: { + translate() {} + } + } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.req.session.user._id) + } + return (this.controller = SandboxedModule.require(modulePath, { + requires: { + './NotificationsHandler': this.handler, + underscore: (this.underscore = { + map(arr) { + return arr + } + }), + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../Authentication/AuthenticationController': this + .AuthenticationController + } + })) + }) + + it('should ask the handler for all unread notifications', function(done) { + const allNotifications = [{ _id: notification_id, user_id }] + this.handler.getUserNotifications = sinon + .stub() + .callsArgWith(1, null, allNotifications) + return this.controller.getAllUnreadNotifications(this.req, { + send: body => { + body.should.equal(allNotifications) + this.handler.getUserNotifications.calledWith(user_id).should.equal(true) + return done() + } + }) + }) + + return it('should send a delete request when a delete has been received to mark a notification', function(done) { + return this.controller.markNotificationAsRead(this.req, { + send: () => { + this.handler.markAsRead + .calledWith(user_id, notification_id) + .should.equal(true) + return done() + } + }) + }) +}) diff --git a/services/web/test/unit/src/Notifications/NotificationsHandlerTests.js b/services/web/test/unit/src/Notifications/NotificationsHandlerTests.js new file mode 100644 index 0000000000..80d0570ccd --- /dev/null +++ b/services/web/test/unit/src/Notifications/NotificationsHandlerTests.js @@ -0,0 +1,185 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const { assert } = require('chai') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Notifications/NotificationsHandler.js' +) +const _ = require('underscore') + +describe('NotificationsHandler', function() { + const user_id = '123nd3ijdks' + const notification_id = '123njdskj9jlk' + const notificationUrl = 'notification.sharelatex.testing' + + beforeEach(function() { + this.request = sinon.stub().callsArgWith(1) + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': { + apis: { notifications: { url: notificationUrl } } + }, + request: this.request, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + describe('getUserNotifications', function() { + it('should get unread notifications', function(done) { + const stubbedNotifications = [{ _id: notification_id, user_id }] + this.request.callsArgWith( + 1, + null, + { statusCode: 200 }, + stubbedNotifications + ) + return this.handler.getUserNotifications( + user_id, + (err, unreadNotifications) => { + stubbedNotifications.should.deep.equal(unreadNotifications) + const getOpts = { + uri: `${notificationUrl}/user/${user_id}`, + json: true, + timeout: 1000, + method: 'GET' + } + this.request.calledWith(getOpts).should.equal(true) + return done() + } + ) + }) + + return it('should return empty arrays if there are no notifications', function() { + this.request.callsArgWith(1, null, { statusCode: 200 }, null) + return this.handler.getUserNotifications( + user_id, + (err, unreadNotifications) => { + return unreadNotifications.length.should.equal(0) + } + ) + }) + }) + + describe('markAsRead', function() { + beforeEach(function() { + return (this.key = 'some key here') + }) + + return it('should send a delete request when a delete has been received to mark a notification', function(done) { + return this.handler.markAsReadWithKey(user_id, this.key, () => { + const opts = { + uri: `${notificationUrl}/user/${user_id}`, + json: { + key: this.key + }, + timeout: 1000, + method: 'DELETE' + } + this.request.calledWith(opts).should.equal(true) + return done() + }) + }) + }) + + describe('createNotification', function() { + beforeEach(function() { + this.key = 'some key here' + this.messageOpts = { value: 12344 } + this.templateKey = 'renderThisHtml' + return (this.expiry = null) + }) + + it('should post the message over', function(done) { + return this.handler.createNotification( + user_id, + this.key, + this.templateKey, + this.messageOpts, + this.expiry, + () => { + const args = this.request.args[0][0] + args.uri.should.equal(`${notificationUrl}/user/${user_id}`) + args.timeout.should.equal(1000) + const expectedJson = { + key: this.key, + templateKey: this.templateKey, + messageOpts: this.messageOpts, + forceCreate: true + } + assert.deepEqual(args.json, expectedJson) + return done() + } + ) + }) + + return describe('when expiry date is supplied', function() { + beforeEach(function() { + this.key = 'some key here' + this.messageOpts = { value: 12344 } + this.templateKey = 'renderThisHtml' + return (this.expiry = new Date()) + }) + + return it('should post the message over with expiry field', function(done) { + return this.handler.createNotification( + user_id, + this.key, + this.templateKey, + this.messageOpts, + this.expiry, + () => { + const args = this.request.args[0][0] + args.uri.should.equal(`${notificationUrl}/user/${user_id}`) + args.timeout.should.equal(1000) + const expectedJson = { + key: this.key, + templateKey: this.templateKey, + messageOpts: this.messageOpts, + expires: this.expiry, + forceCreate: true + } + assert.deepEqual(args.json, expectedJson) + return done() + } + ) + }) + }) + }) + + return describe('markAsReadByKeyOnly', function() { + beforeEach(function() { + return (this.key = 'some key here') + }) + + return it('should send a delete request when a delete has been received to mark a notification', function(done) { + return this.handler.markAsReadByKeyOnly(this.key, () => { + const opts = { + uri: `${notificationUrl}/key/${this.key}`, + timeout: 1000, + method: 'DELETE' + } + this.request.calledWith(opts).should.equal(true) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.js b/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.js new file mode 100644 index 0000000000..17999a4353 --- /dev/null +++ b/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.js @@ -0,0 +1,379 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +let { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/PasswordReset/PasswordResetController' +) +;({ expect } = require('chai')) + +describe('PasswordResetController', function() { + beforeEach(function() { + this.settings = {} + this.PasswordResetHandler = { + generateAndEmailResetToken: sinon.stub(), + setNewUserPassword: sinon.stub() + } + this.RateLimiter = { addCount: sinon.stub() } + this.UserSessionsManager = { + revokeAllUserSessions: sinon.stub().callsArgWith(2, null) + } + this.AuthenticationManager = { validatePassword: sinon.stub() } + this.UserUpdater = { + removeReconfirmFlag: sinon.stub().callsArgWith(1, null) + } + this.PasswordResetController = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + './PasswordResetHandler': this.PasswordResetHandler, + 'logger-sharelatex': { + log() {} + }, + '../../infrastructure/RateLimiter': this.RateLimiter, + '../Authentication/AuthenticationController': (this.AuthenticationController = {}), + '../Authentication/AuthenticationManager': this.AuthenticationManager, + '../User/UserGetter': (this.UserGetter = {}), + '../User/UserSessionsManager': this.UserSessionsManager, + '../User/UserUpdater': this.UserUpdater + } + }) + + this.email = 'bob@bob.com ' + this.user_id = 'mock-user-id' + this.token = 'my security token that was emailed to me' + this.password = 'my new password' + this.req = { + body: { + email: this.email, + passwordResetToken: this.token, + password: this.password + }, + i18n: { + translate() {} + }, + session: {}, + query: {} + } + + return (this.res = {}) + }) + + describe('requestReset', function() { + it('should error if the rate limit is hit', function(done) { + this.PasswordResetHandler.generateAndEmailResetToken.callsArgWith( + 1, + null, + 'primary' + ) + this.RateLimiter.addCount.callsArgWith(1, null, false) + this.res.send = code => { + code.should.equal(429) + this.PasswordResetHandler.generateAndEmailResetToken + .calledWith(this.email.trim()) + .should.equal(false) + return done() + } + return this.PasswordResetController.requestReset(this.req, this.res) + }) + + it('should tell the handler to process that email', function(done) { + this.RateLimiter.addCount.callsArgWith(1, null, true) + this.PasswordResetHandler.generateAndEmailResetToken.callsArgWith( + 1, + null, + 'primary' + ) + this.res.send = code => { + code.should.equal(200) + this.PasswordResetHandler.generateAndEmailResetToken + .calledWith(this.email.trim()) + .should.equal(true) + return done() + } + return this.PasswordResetController.requestReset(this.req, this.res) + }) + + it('should send a 500 if there is an error', function(done) { + this.RateLimiter.addCount.callsArgWith(1, null, true) + this.PasswordResetHandler.generateAndEmailResetToken.callsArgWith( + 1, + 'error' + ) + this.res.send = code => { + code.should.equal(500) + return done() + } + return this.PasswordResetController.requestReset(this.req, this.res) + }) + + it("should send a 404 if the email doesn't exist", function(done) { + this.RateLimiter.addCount.callsArgWith(1, null, true) + this.PasswordResetHandler.generateAndEmailResetToken.callsArgWith( + 1, + null, + null + ) + this.res.send = code => { + code.should.equal(404) + return done() + } + return this.PasswordResetController.requestReset(this.req, this.res) + }) + + it('should send a 404 if the email is registered as a secondard email', function(done) { + this.RateLimiter.addCount.callsArgWith(1, null, true) + this.PasswordResetHandler.generateAndEmailResetToken.callsArgWith( + 1, + null, + 'secondary' + ) + this.res.send = code => { + code.should.equal(404) + return done() + } + return this.PasswordResetController.requestReset(this.req, this.res) + }) + + return it('should lowercase the email address', function(done) { + this.email = 'UPerCaseEMAIL@example.Com' + this.req.body.email = this.email + this.RateLimiter.addCount.callsArgWith(1, null, true) + this.PasswordResetHandler.generateAndEmailResetToken.callsArgWith( + 1, + null, + 'primary' + ) + this.res.send = code => { + code.should.equal(200) + this.PasswordResetHandler.generateAndEmailResetToken + .calledWith(this.email.toLowerCase()) + .should.equal(true) + return done() + } + return this.PasswordResetController.requestReset(this.req, this.res) + }) + }) + + describe('setNewUserPassword', function() { + beforeEach(function() { + return (this.req.session.resetToken = this.token) + }) + + it('should tell the user handler to reset the password', function(done) { + this.PasswordResetHandler.setNewUserPassword.callsArgWith( + 2, + null, + true, + this.user_id + ) + this.res.sendStatus = code => { + code.should.equal(200) + this.PasswordResetHandler.setNewUserPassword + .calledWith(this.token, this.password) + .should.equal(true) + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + it("should send 404 if the token didn't work", function(done) { + this.PasswordResetHandler.setNewUserPassword.callsArgWith( + 2, + null, + false, + this.user_id + ) + this.res.sendStatus = code => { + code.should.equal(404) + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + it('should return 400 (Bad Request) if there is no password', function(done) { + this.req.body.password = '' + this.PasswordResetHandler.setNewUserPassword.callsArgWith(2) + this.res.sendStatus = code => { + code.should.equal(400) + this.PasswordResetHandler.setNewUserPassword.called.should.equal(false) + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + it('should return 400 (Bad Request) if there is no passwordResetToken', function(done) { + this.req.body.passwordResetToken = '' + this.PasswordResetHandler.setNewUserPassword.callsArgWith(2) + this.res.sendStatus = code => { + code.should.equal(400) + this.PasswordResetHandler.setNewUserPassword.called.should.equal(false) + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + it('should return 400 (Bad Request) if the password is invalid', function(done) { + this.req.body.password = 'correct horse battery staple' + this.AuthenticationManager.validatePassword = sinon + .stub() + .returns({ message: 'password contains invalid characters' }) + this.PasswordResetHandler.setNewUserPassword.callsArgWith(2) + this.res.sendStatus = code => { + code.should.equal(400) + this.PasswordResetHandler.setNewUserPassword.called.should.equal(false) + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + it('should clear the session.resetToken', function(done) { + this.PasswordResetHandler.setNewUserPassword.callsArgWith( + 2, + null, + true, + this.user_id + ) + this.res.sendStatus = code => { + code.should.equal(200) + this.req.session.should.not.have.property('resetToken') + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + it('should clear sessions', function(done) { + this.PasswordResetHandler.setNewUserPassword.callsArgWith( + 2, + null, + true, + this.user_id + ) + this.res.sendStatus = code => { + this.UserSessionsManager.revokeAllUserSessions.callCount.should.equal(1) + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + it('should call removeReconfirmFlag', function(done) { + this.PasswordResetHandler.setNewUserPassword.callsArgWith( + 2, + null, + true, + this.user_id + ) + this.res.sendStatus = code => { + this.UserUpdater.removeReconfirmFlag.callCount.should.equal(1) + return done() + } + return this.PasswordResetController.setNewUserPassword(this.req, this.res) + }) + + return describe('when login_after is set', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, { email: 'joe@example.com' }) + this.PasswordResetHandler.setNewUserPassword.callsArgWith( + 2, + null, + true, + (this.user_id = 'user-id-123') + ) + this.req.body.login_after = 'true' + this.res.json = sinon.stub() + this.AuthenticationController.afterLoginSessionSetup = sinon + .stub() + .callsArgWith(2, null) + return (this.AuthenticationController._getRedirectFromSession = sinon + .stub() + .returns('/some/path')) + }) + + return it('should login user if login_after is set', function(done) { + this.PasswordResetController.setNewUserPassword(this.req, this.res) + this.AuthenticationController.afterLoginSessionSetup.callCount.should.equal( + 1 + ) + this.AuthenticationController.afterLoginSessionSetup + .calledWith(this.req, { email: 'joe@example.com' }) + .should.equal(true) + this.AuthenticationController._getRedirectFromSession.callCount.should.equal( + 1 + ) + this.res.json.callCount.should.equal(1) + this.res.json.calledWith({ redir: '/some/path' }).should.equal(true) + return done() + }) + }) + }) + + return describe('renderSetPasswordForm', function() { + describe('with token in query-string', function() { + beforeEach(function() { + return (this.req.query.passwordResetToken = this.token) + }) + + return it('should set session.resetToken and redirect', function(done) { + this.req.session.should.not.have.property('resetToken') + this.res.redirect = path => { + path.should.equal('/user/password/set') + this.req.session.resetToken.should.equal(this.token) + return done() + } + return this.PasswordResetController.renderSetPasswordForm( + this.req, + this.res + ) + }) + }) + + return describe('without a token in query-string', function() { + describe('with token in session', function() { + beforeEach(function() { + return (this.req.session.resetToken = this.token) + }) + + return it('should render the page, passing the reset token', function(done) { + this.res.render = (template_path, options) => { + options.passwordResetToken.should.equal(this.req.session.resetToken) + return done() + } + return this.PasswordResetController.renderSetPasswordForm( + this.req, + this.res + ) + }) + }) + + return describe('without a token in session', () => + it('should redirect to the reset request page', function(done) { + this.res.redirect = path => { + path.should.equal('/user/password/reset') + this.req.session.should.not.have.property('resetToken') + return done() + } + return this.PasswordResetController.renderSetPasswordForm( + this.req, + this.res + ) + })) + }) + }) +}) diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetHandlerTests.js b/services/web/test/unit/src/PasswordReset/PasswordResetHandlerTests.js new file mode 100644 index 0000000000..a698823b24 --- /dev/null +++ b/services/web/test/unit/src/PasswordReset/PasswordResetHandlerTests.js @@ -0,0 +1,359 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/PasswordReset/PasswordResetHandler' +) +const { expect } = require('chai') + +describe('PasswordResetHandler', function() { + beforeEach(function() { + this.settings = { siteUrl: 'www.sharelatex.com' } + this.OneTimeTokenHandler = { + getNewToken: sinon.stub(), + getValueFromTokenAndExpire: sinon.stub() + } + this.UserGetter = { + getUserByMainEmail: sinon.stub(), + getUser: sinon.stub(), + getUserByAnyEmail: sinon.stub() + } + this.EmailHandler = { sendEmail: sinon.stub() } + this.AuthenticationManager = { + setUserPassword: sinon.stub(), + setUserPasswordInV1: sinon.stub(), + setUserPasswordInV2: sinon.stub() + } + this.V1Api = { request: sinon.stub() } + this.PasswordResetHandler = SandboxedModule.require(modulePath, { + requires: { + '../User/UserGetter': this.UserGetter, + '../Security/OneTimeTokenHandler': this.OneTimeTokenHandler, + '../Email/EmailHandler': this.EmailHandler, + '../Authentication/AuthenticationManager': this.AuthenticationManager, + '../V1/V1Api': this.V1Api, + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + }) + this.token = '12312321i' + this.user_id = 'user_id_here' + this.user = { email: (this.email = 'bob@bob.com') } + this.password = 'my great secret password' + return (this.callback = sinon.stub()) + }) + + describe('generateAndEmailResetToken', function() { + describe('when in ShareLaTeX', function() { + it('should check the user exists', function(done) { + this.UserGetter.getUserByMainEmail.callsArgWith(1) + this.UserGetter.getUserByAnyEmail.callsArgWith(1) + this.OneTimeTokenHandler.getNewToken.yields() + return this.PasswordResetHandler.generateAndEmailResetToken( + this.user.email, + (err, status) => { + should.equal(status, null) + return done() + } + ) + }) + + it('should send the email with the token', function(done) { + this.UserGetter.getUserByMainEmail.callsArgWith(1, null, this.user) + this.OneTimeTokenHandler.getNewToken.yields(null, this.token) + this.EmailHandler.sendEmail.callsArgWith(2) + return this.PasswordResetHandler.generateAndEmailResetToken( + this.user.email, + (err, status) => { + this.EmailHandler.sendEmail.called.should.equal(true) + status.should.equal('primary') + const args = this.EmailHandler.sendEmail.args[0] + args[0].should.equal('passwordResetRequested') + args[1].setNewPasswordUrl.should.equal( + `${this.settings.siteUrl}/user/password/set?passwordResetToken=${ + this.token + }&email=${encodeURIComponent(this.user.email)}` + ) + return done() + } + ) + }) + + return it('should return exists == null for a holdingAccount', function(done) { + this.user.holdingAccount = true + this.UserGetter.getUserByMainEmail.callsArgWith(1, null, this.user) + this.UserGetter.getUserByAnyEmail.callsArgWith(1) + this.OneTimeTokenHandler.getNewToken.yields() + return this.PasswordResetHandler.generateAndEmailResetToken( + this.user.email, + (err, status) => { + should.equal(status, null) + return done() + } + ) + }) + }) + + return describe('when in overleaf', function() { + beforeEach(function() { + return (this.settings.overleaf = true) + }) + + describe('when the email exists', function() { + beforeEach(function() { + this.V1Api.request.yields(null, {}, { user_id: 42 }) + this.OneTimeTokenHandler.getNewToken.yields(null, this.token) + this.EmailHandler.sendEmail.yields() + return this.PasswordResetHandler.generateAndEmailResetToken( + this.email, + this.callback + ) + }) + + it('should call the v1 api for the user', function() { + return this.V1Api.request + .calledWith({ + url: '/api/v1/sharelatex/user_emails', + qs: { + email: this.email + }, + expectedStatusCodes: [404] + }) + .should.equal(true) + }) + + it('should set the password token data to the user id and email', function() { + return this.OneTimeTokenHandler.getNewToken + .calledWith('password', { + v1_user_id: 42 + }) + .should.equal(true) + }) + + it('should send an email with the token', function() { + this.EmailHandler.sendEmail.called.should.equal(true) + const args = this.EmailHandler.sendEmail.args[0] + args[0].should.equal('passwordResetRequested') + return args[1].setNewPasswordUrl.should.equal( + `${this.settings.siteUrl}/user/password/set?passwordResetToken=${ + this.token + }&email=${encodeURIComponent(this.user.email)}` + ) + }) + + return it('should return status == true', function() { + return this.callback.calledWith(null, 'primary').should.equal(true) + }) + }) + + describe("when the email doesn't exist", function() { + beforeEach(function() { + this.V1Api.request = sinon + .stub() + .yields(null, { statusCode: 404 }, {}) + this.UserGetter.getUserByAnyEmail.callsArgWith(1) + return this.PasswordResetHandler.generateAndEmailResetToken( + this.email, + this.callback + ) + }) + + it('should not set the password token data', function() { + return this.OneTimeTokenHandler.getNewToken.called.should.equal(false) + }) + + it('should send an email with the token', function() { + return this.EmailHandler.sendEmail.called.should.equal(false) + }) + + return it('should return status == null', function() { + return this.callback.calledWith(null, null).should.equal(true) + }) + }) + + describe("when the user isn't on v2", function() { + beforeEach(function() { + this.V1Api.request = sinon + .stub() + .yields(null, { statusCode: 404 }, {}) + this.UserGetter.getUserByAnyEmail.callsArgWith(1, null, this.user) + return this.PasswordResetHandler.generateAndEmailResetToken( + this.email, + this.callback + ) + }) + + it('should not set the password token data', function() { + return this.OneTimeTokenHandler.getNewToken.called.should.equal(false) + }) + + it('should not send an email with the token', function() { + return this.EmailHandler.sendEmail.called.should.equal(false) + }) + + return it('should return status == sharelatex', function() { + return this.callback.calledWith(null, 'sharelatex').should.equal(true) + }) + }) + + return describe('when the email is a secondary email', function() { + beforeEach(function() { + this.V1Api.request = sinon + .stub() + .yields(null, { statusCode: 404 }, {}) + this.user.overleaf = { id: 101 } + this.UserGetter.getUserByAnyEmail.callsArgWith(1, null, this.user) + return this.PasswordResetHandler.generateAndEmailResetToken( + this.email, + this.callback + ) + }) + + it('should not set the password token data', function() { + return this.OneTimeTokenHandler.getNewToken.called.should.equal(false) + }) + + it('should not send an email with the token', function() { + return this.EmailHandler.sendEmail.called.should.equal(false) + }) + + return it('should return status == secondary', function() { + return this.callback.calledWith(null, 'secondary').should.equal(true) + }) + }) + }) + }) + + return describe('setNewUserPassword', function() { + describe('when no data is found', function() { + beforeEach(function() { + this.OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, null) + return this.PasswordResetHandler.setNewUserPassword( + this.token, + this.password, + this.callback + ) + }) + + return it('should return exists == false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + + describe('when the data is an old style user_id', function() { + beforeEach(function() { + this.AuthenticationManager.setUserPassword.yields( + null, + true, + this.user_id + ) + this.OneTimeTokenHandler.getValueFromTokenAndExpire.yields( + null, + this.user_id + ) + return this.PasswordResetHandler.setNewUserPassword( + this.token, + this.password, + this.callback + ) + }) + + it('should call setUserPasswordInV2', function() { + return this.AuthenticationManager.setUserPassword + .calledWith(this.user_id, this.password) + .should.equal(true) + }) + + return it('should reset == true and the user_id', function() { + return this.callback + .calledWith(null, true, this.user_id) + .should.equal(true) + }) + }) + + describe('when the data is a new style user_id', function() { + beforeEach(function() { + this.AuthenticationManager.setUserPassword.yields( + null, + true, + this.user_id + ) + this.OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, { + user_id: this.user_id + }) + return this.PasswordResetHandler.setNewUserPassword( + this.token, + this.password, + this.callback + ) + }) + + it('should call setUserPasswordInV2', function() { + return this.AuthenticationManager.setUserPassword + .calledWith(this.user_id, this.password) + .should.equal(true) + }) + + return it('should reset == true and the user_id', function() { + return this.callback + .calledWith(null, true, this.user_id) + .should.equal(true) + }) + }) + + return describe('when the data is v1 id', function() { + beforeEach(function() { + this.v1_user_id = 2345 + this.AuthenticationManager.setUserPasswordInV1.yields(null, true) + this.UserGetter.getUser + .withArgs({ 'overleaf.id': this.v1_user_id }) + .yields(null, { _id: this.user_id }) + this.OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, { + v1_user_id: this.v1_user_id + }) + return this.PasswordResetHandler.setNewUserPassword( + this.token, + this.password, + this.callback + ) + }) + + it('should call setUserPasswordInV1', function() { + return this.AuthenticationManager.setUserPasswordInV1 + .calledWith(this.v1_user_id, this.password) + .should.equal(true) + }) + + it('should look up the user by v1 id for the v2 user id', function() { + return this.UserGetter.getUser + .calledWith({ 'overleaf.id': this.v1_user_id }) + .should.equal(true) + }) + + return it('should reset == true and the user_id', function() { + return this.callback + .calledWith(null, true, this.user_id) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/DocLinesComparitorTests.js b/services/web/test/unit/src/Project/DocLinesComparitorTests.js new file mode 100644 index 0000000000..f3a18dbbd3 --- /dev/null +++ b/services/web/test/unit/src/Project/DocLinesComparitorTests.js @@ -0,0 +1,98 @@ +/* eslint-disable + max-len, + mocha/no-identical-title, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/src/Features/Project/DocLinesComparitor.js' +const SandboxedModule = require('sandboxed-module') + +describe('doc lines comparitor', function() { + beforeEach(function() { + return (this.comparitor = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { log() {} } + } + })) + }) + + it('should return true when the lines are the same', function() { + const lines1 = ['hello', 'world'] + const lines2 = ['hello', 'world'] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(true) + }) + + it('should return false when the lines are different', function() { + const lines1 = ['hello', 'world'] + const lines2 = ['diff', 'world'] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(false) + }) + + it('should return false when the lines are different', function() { + const lines1 = ['hello', 'world'] + const lines2 = ['hello', 'wrld'] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(false) + }) + + it('should return true when the lines are same', function() { + const lines1 = ['hello', 'world'] + const lines2 = ['hello', 'world'] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(true) + }) + + it('should return false if the doc lines are different in length', function() { + const lines1 = ['hello', 'world'] + const lines2 = ['hello', 'world', 'please'] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(false) + }) + + it('should return false if the first array is undefined', function() { + const lines1 = undefined + const lines2 = ['hello', 'world'] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(false) + }) + + it('should return false if the second array is undefined', function() { + const lines1 = ['hello'] + const lines2 = undefined + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(false) + }) + + it('should return false if the second array is not an array', function() { + const lines1 = ['hello'] + const lines2 = '' + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(false) + }) + + it('should return true when comparing equal orchard docs', function() { + const lines1 = [{ text: 'hello world' }] + const lines2 = [{ text: 'hello world' }] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(true) + }) + + return it('should return false when comparing different orchard docs', function() { + const lines1 = [{ text: 'goodbye world' }] + const lines2 = [{ text: 'hello world' }] + const result = this.comparitor.areSame(lines1, lines2) + return result.should.equal(false) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectApiControllerTests.js b/services/web/test/unit/src/Project/ProjectApiControllerTests.js new file mode 100644 index 0000000000..5c7bda1c57 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectApiControllerTests.js @@ -0,0 +1,67 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const modulePath = '../../../../app/src/Features/Project/ProjectApiController' +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() + +describe('Project api controller', function() { + beforeEach(function() { + this.ProjectDetailsHandler = { getDetails: sinon.stub() } + this.controller = SandboxedModule.require(modulePath, { + requires: { + './ProjectDetailsHandler': this.ProjectDetailsHandler, + 'logger-sharelatex': { + log() {} + } + } + }) + this.project_id = '321l3j1kjkjl' + this.req = { + params: { + project_id: this.project_id + }, + session: { + destroy: sinon.stub() + } + } + this.res = {} + this.next = sinon.stub() + return (this.projDetails = { name: 'something' }) + }) + + return describe('getProjectDetails', function() { + it('should ask the project details handler for proj details', function(done) { + this.ProjectDetailsHandler.getDetails.callsArgWith( + 1, + null, + this.projDetails + ) + this.res.json = data => { + this.ProjectDetailsHandler.getDetails + .calledWith(this.project_id) + .should.equal(true) + data.should.deep.equal(this.projDetails) + return done() + } + return this.controller.getProjectDetails(this.req, this.res) + }) + + return it('should send a 500 if there is an error', function() { + this.ProjectDetailsHandler.getDetails.callsArgWith(1, 'error') + this.controller.getProjectDetails(this.req, this.res, this.next) + return this.next.calledWith('error').should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectCollabratecDetailsTest.js b/services/web/test/unit/src/Project/ProjectCollabratecDetailsTest.js new file mode 100644 index 0000000000..f01e1c0d2e --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectCollabratecDetailsTest.js @@ -0,0 +1,511 @@ +/* eslint-disable + max-len, + mocha/no-identical-title, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { ObjectId } = require('mongojs') +const Path = require('path') +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const chai = require('chai') +const sinon = require('sinon') + +const { expect } = chai + +const modulePath = Path.join( + __dirname, + '../../../../app/src/Features/Project/ProjectCollabratecDetailsHandler' +) + +describe('ProjectCollabratecDetailsHandler', function() { + beforeEach(function() { + this.projectId = ObjectId('5bea8747c7bba6012fcaceb3') + this.userId = ObjectId('5be316a9c7f6aa03802ea8fb') + this.userId2 = ObjectId('5c1794b3f0e89b1d1c577eca') + this.ProjectModel = {} + this.ProjectCollabratecDetailsHandler = SandboxedModule.require( + modulePath, + { + requires: { + '../../models/Project': { Project: this.ProjectModel } + } + } + ) + return (this.callback = sinon.stub()) + }) + + describe('initializeCollabratecProject', function() { + describe('when update succeeds', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields() + return this.ProjectCollabratecDetailsHandler.initializeCollabratecProject( + this.projectId, + this.userId, + 'collabratec-document-id', + 'collabratec-private-group-id', + this.callback + ) + }) + + return it('should update project model', function() { + const update = { + $set: { + collabratecUsers: [ + { + user_id: this.userId, + collabratec_document_id: 'collabratec-document-id', + collabratec_privategroup_id: 'collabratec-private-group-id' + } + ] + } + } + return expect(this.ProjectModel.update).to.have.been.calledWith( + { _id: this.projectId }, + update, + this.callback + ) + }) + }) + + describe('when update has error', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields('error') + return this.ProjectCollabratecDetailsHandler.initializeCollabratecProject( + this.projectId, + this.userId, + 'collabratec-document-id', + 'collabratec-private-group-id', + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback).to.have.been.calledWith('error') + }) + }) + + return describe('with invalid args', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub() + return this.ProjectCollabratecDetailsHandler.initializeCollabratecProject( + 'bad-project-id', + 'bad-user-id', + 'collabratec-document-id', + 'collabratec-private-group-id', + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.update).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + + describe('isLinkedCollabratecUserProject', function() { + beforeEach(function() { + return (this.ProjectModel.findOne = sinon.stub().yields()) + }) + + describe('when find succeeds', function() { + describe('when user project found', function() { + beforeEach(function() { + this.ProjectModel.findOne = sinon.stub().yields(null, 'project') + return this.ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject( + this.projectId, + this.userId, + this.callback + ) + }) + + it('should call find with project and user id', function() { + return expect(this.ProjectModel.findOne).to.have.been.calledWithMatch( + { + _id: ObjectId(this.projectId), + collabratecUsers: { + $elemMatch: { + user_id: ObjectId(this.userId) + } + } + } + ) + }) + + return it('should callback with true', function() { + return expect(this.callback).to.have.been.calledWith(null, true) + }) + }) + + return describe('when user project found', function() { + beforeEach(function() { + this.ProjectModel.findOne = sinon.stub().yields(null, null) + return this.ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject( + this.projectId, + this.userId, + this.callback + ) + }) + + return it('should callback with false', function() { + return expect(this.callback).to.have.been.calledWith(null, false) + }) + }) + }) + + describe('when find has error', function() { + beforeEach(function() { + this.ProjectModel.findOne = sinon.stub().yields('error') + return this.ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject( + this.projectId, + this.userId, + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback).to.have.been.calledWith('error') + }) + }) + + return describe('with invalid args', function() { + beforeEach(function() { + this.ProjectModel.findOne = sinon.stub() + return this.ProjectCollabratecDetailsHandler.isLinkedCollabratecUserProject( + 'bad-project-id', + 'bad-user-id', + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.findOne).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + + describe('linkCollabratecUserProject', function() { + describe('when update succeeds', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields() + return this.ProjectCollabratecDetailsHandler.linkCollabratecUserProject( + this.projectId, + this.userId, + 'collabratec-document-id', + this.callback + ) + }) + + return it('should update project model', function() { + const query = { + _id: this.projectId, + collabratecUsers: { + $not: { + $elemMatch: { + collabratec_document_id: 'collabratec-document-id', + user_id: this.userId + } + } + } + } + const update = { + $push: { + collabratecUsers: { + collabratec_document_id: 'collabratec-document-id', + user_id: this.userId + } + } + } + return expect(this.ProjectModel.update).to.have.been.calledWith( + query, + update, + this.callback + ) + }) + }) + + describe('when update has error', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields('error') + return this.ProjectCollabratecDetailsHandler.linkCollabratecUserProject( + this.projectId, + this.userId, + 'collabratec-document-id', + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback).to.have.been.calledWith('error') + }) + }) + + return describe('with invalid args', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub() + return this.ProjectCollabratecDetailsHandler.linkCollabratecUserProject( + 'bad-project-id', + 'bad-user-id', + 'collabratec-document-id', + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.update).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + + describe('setCollabratecUsers', function() { + beforeEach(function() { + return (this.collabratecUsers = [ + { + user_id: this.userId, + collabratec_document_id: 'collabratec-document-id-1', + collabratec_privategroup_id: 'collabratec-private-group-id-1' + }, + { + user_id: this.userId2, + collabratec_document_id: 'collabratec-document-id-2', + collabratec_privategroup_id: 'collabratec-private-group-id-2' + } + ]) + }) + + describe('when update succeeds', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields() + return this.ProjectCollabratecDetailsHandler.setCollabratecUsers( + this.projectId, + this.collabratecUsers, + this.callback + ) + }) + + return it('should update project model', function() { + const update = { + $set: { + collabratecUsers: this.collabratecUsers + } + } + return expect(this.ProjectModel.update).to.have.been.calledWith( + { _id: this.projectId }, + update, + this.callback + ) + }) + }) + + describe('when update has error', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields('error') + return this.ProjectCollabratecDetailsHandler.setCollabratecUsers( + this.projectId, + this.collabratecUsers, + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback).to.have.been.calledWith('error') + }) + }) + + describe('with invalid project_id', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub() + return this.ProjectCollabratecDetailsHandler.setCollabratecUsers( + 'bad-project-id', + this.collabratecUsers, + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.update).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + + return describe('with invalid user_id', function() { + beforeEach(function() { + this.collabratecUsers[1].user_id = 'bad-user-id' + this.ProjectModel.update = sinon.stub() + return this.ProjectCollabratecDetailsHandler.setCollabratecUsers( + this.projectId, + this.collabratecUsers, + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.update).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + + describe('unlinkCollabratecUserProject', function() { + describe('when update succeeds', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields() + return this.ProjectCollabratecDetailsHandler.unlinkCollabratecUserProject( + this.projectId, + this.userId, + this.callback + ) + }) + + return it('should update project model', function() { + const query = { _id: this.projectId } + const update = { + $pull: { + collabratecUsers: { + user_id: this.userId + } + } + } + return expect(this.ProjectModel.update).to.have.been.calledWith( + query, + update, + this.callback + ) + }) + }) + + describe('when update has error', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields('error') + return this.ProjectCollabratecDetailsHandler.unlinkCollabratecUserProject( + this.projectId, + this.userId, + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback).to.have.been.calledWith('error') + }) + }) + + return describe('with invalid args', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub() + return this.ProjectCollabratecDetailsHandler.unlinkCollabratecUserProject( + 'bad-project-id', + 'bad-user-id', + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.update).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + + return describe('updateCollabratecUserIds', function() { + describe('when update succeeds', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields() + return this.ProjectCollabratecDetailsHandler.updateCollabratecUserIds( + this.userId, + this.userId2, + this.callback + ) + }) + + return it('should update project model', function() { + return expect(this.ProjectModel.update).to.have.been.calledWith( + { 'collabratecUsers.user_id': this.userId }, + { $set: { 'collabratecUsers.$.user_id': this.userId2 } }, + { multi: true }, + this.callback + ) + }) + }) + + describe('when update has error', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub().yields('error') + return this.ProjectCollabratecDetailsHandler.updateCollabratecUserIds( + this.userId, + this.userId2, + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback).to.have.been.calledWith('error') + }) + }) + + describe('with invalid old_user_id', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub() + return this.ProjectCollabratecDetailsHandler.updateCollabratecUserIds( + 'bad-user-id', + this.userId2, + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.update).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + + return describe('with invalid new_user_id', function() { + beforeEach(function() { + this.ProjectModel.update = sinon.stub() + return this.ProjectCollabratecDetailsHandler.updateCollabratecUserIds( + this.userId, + 'bad-user-id', + this.callback + ) + }) + + it('should not update', function() { + return expect(this.ProjectModel.update).not.to.have.beenCalled + }) + + return it('should callback with error', function() { + return expect(this.callback.firstCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js new file mode 100644 index 0000000000..e66bedc747 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -0,0 +1,1047 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Project/ProjectController' +) +const { expect } = require('chai') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('ProjectController', function() { + beforeEach(function() { + this.project_id = '123213jlkj9kdlsaj' + + this.user = { + _id: '588f3ddae8ebc1bac07c9fa4', + first_name: 'bjkdsjfk', + features: {} + } + this.settings = { + apis: { + chat: { + url: 'chat.com' + } + }, + siteUrl: 'mysite.com' + } + this.brandVariationDetails = { + id: '12', + active: true, + brand_name: 'The journal', + home_url: 'http://www.thejournal.com/', + publish_menu_link_html: 'Submit your paper to the The Journal' + } + this.token = 'some-token' + this.ProjectDeleter = { + archiveProject: sinon.stub().callsArg(1), + deleteProject: sinon.stub().callsArg(2), + restoreProject: sinon.stub().callsArg(1), + findArchivedProjects: sinon.stub() + } + this.ProjectDuplicator = { + duplicate: sinon.stub().callsArgWith(3, null, { _id: this.project_id }) + } + this.ProjectCreationHandler = { + createExampleProject: sinon + .stub() + .callsArgWith(2, null, { _id: this.project_id }), + createBasicProject: sinon + .stub() + .callsArgWith(2, null, { _id: this.project_id }) + } + this.SubscriptionLocator = { getUsersSubscription: sinon.stub() } + this.LimitationsManager = { hasPaidSubscription: sinon.stub() } + this.TagsHandler = { getAllTags: sinon.stub() } + this.NotificationsHandler = { getUserNotifications: sinon.stub() } + this.UserModel = { findById: sinon.stub() } + this.AuthorizationManager = { getPrivilegeLevelForProject: sinon.stub() } + this.EditorController = { renameProject: sinon.stub() } + this.InactiveProjectManager = { reactivateProjectIfRequired: sinon.stub() } + this.ProjectUpdateHandler = { markAsOpened: sinon.stub() } + this.ReferencesSearchHandler = { indexProjectReferences: sinon.stub() } + this.ProjectGetter = { + findAllUsersProjects: sinon.stub(), + getProject: sinon.stub() + } + this.AuthenticationController = { + getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user), + getLoggedInUserId: sinon.stub().returns(this.user._id), + getSessionUser: sinon.stub().returns(this.user), + isUserLoggedIn: sinon.stub().returns(true) + } + this.AnalyticsManager = { getLastOccurrence: sinon.stub() } + this.TokenAccessHandler = { + getRequestToken: sinon.stub().returns(this.token), + protectTokens: sinon.stub() + } + this.CollaboratorsHandler = { + userIsTokenMember: sinon.stub().callsArgWith(2, null, false) + } + this.ProjectEntityHandler = {} + this.NotificationBuilder = { + ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }) + } + this.UserGetter = { + getUser: sinon + .stub() + .callsArgWith(2, null, { lastLoginIp: '192.170.18.2' }), + getUserOrUserStubById: sinon.stub().callsArgWith(2, null, {}) + } + this.Modules = { + hooks: { + fire: sinon.stub() + } + } + this.Features = { hasFeature: sinon.stub() } + this.BrandVariationsHandler = { + getBrandVariationById: sinon + .stub() + .callsArgWith(1, null, this.brandVariationDetails) + } + this.getUserAffiliations = sinon.stub().callsArgWith(1, null, []) + + this.ProjectController = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {}, + err() {} + }, + 'metrics-sharelatex': { + Timer: class { + done() {} + }, + inc() {} + }, + './ProjectDeleter': this.ProjectDeleter, + './ProjectDuplicator': this.ProjectDuplicator, + './ProjectCreationHandler': this.ProjectCreationHandler, + '../Editor/EditorController': this.EditorController, + '../Subscription/SubscriptionLocator': this.SubscriptionLocator, + '../Subscription/LimitationsManager': this.LimitationsManager, + '../Tags/TagsHandler': this.TagsHandler, + '../Notifications/NotificationsHandler': this.NotificationsHandler, + '../../models/User': { + User: this.UserModel + }, + '../Authorization/AuthorizationManager': this.AuthorizationManager, + '../InactiveData/InactiveProjectManager': this.InactiveProjectManager, + './ProjectUpdateHandler': this.ProjectUpdateHandler, + '../ReferencesSearch/ReferencesSearchHandler': this + .ReferencesSearchHandler, + './ProjectGetter': this.ProjectGetter, + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../Analytics/AnalyticsManager': this.AnalyticsManager, + '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler, + '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + '../../infrastructure/Modules': this.Modules, + './ProjectEntityHandler': this.ProjectEntityHandler, + '../Errors/Errors': Errors, + '../../infrastructure/Features': this.Features, + '../Notifications/NotificationsBuilder': this.NotificationBuilder, + '../User/UserGetter': this.UserGetter, + '../BrandVariations/BrandVariationsHandler': this + .BrandVariationsHandler, + '../Institutions/InstitutionsAPI': { + getUserAffiliations: this.getUserAffiliations + }, + '../V1/V1Handler': {} + } + }) + + this.projectName = '£12321jkj9ujkljds' + this.req = { + params: { + Project_id: this.project_id + }, + headers: {}, + connection: { + remoteAddress: '192.170.18.1' + }, + session: { + user: this.user + }, + body: { + projectName: this.projectName + }, + i18n: { + translate() {} + }, + ip: '192.170.18.1' + } + return (this.res = { + locals: { + jsPath: 'js path here' + }, + setTimeout: sinon.stub() + }) + }) + + describe('updateProjectSettings', function() { + it('should update the name', function(done) { + this.EditorController.renameProject = sinon.stub().callsArg(2) + this.req.body = { name: (this.name = 'New name') } + this.res.sendStatus = code => { + this.EditorController.renameProject + .calledWith(this.project_id, this.name) + .should.equal(true) + code.should.equal(204) + return done() + } + return this.ProjectController.updateProjectSettings(this.req, this.res) + }) + + it('should update the compiler', function(done) { + this.EditorController.setCompiler = sinon.stub().callsArg(2) + this.req.body = { compiler: (this.compiler = 'pdflatex') } + this.res.sendStatus = code => { + this.EditorController.setCompiler + .calledWith(this.project_id, this.compiler) + .should.equal(true) + code.should.equal(204) + return done() + } + return this.ProjectController.updateProjectSettings(this.req, this.res) + }) + + it('should update the imageName', function(done) { + this.EditorController.setImageName = sinon.stub().callsArg(2) + this.req.body = { imageName: (this.imageName = 'texlive-1234.5') } + this.res.sendStatus = code => { + this.EditorController.setImageName + .calledWith(this.project_id, this.imageName) + .should.equal(true) + code.should.equal(204) + return done() + } + return this.ProjectController.updateProjectSettings(this.req, this.res) + }) + + it('should update the spell check language', function(done) { + this.EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2) + this.req.body = { spellCheckLanguage: (this.languageCode = 'fr') } + this.res.sendStatus = code => { + this.EditorController.setSpellCheckLanguage + .calledWith(this.project_id, this.languageCode) + .should.equal(true) + code.should.equal(204) + return done() + } + return this.ProjectController.updateProjectSettings(this.req, this.res) + }) + + return it('should update the root doc', function(done) { + this.EditorController.setRootDoc = sinon.stub().callsArg(2) + this.req.body = { rootDocId: (this.rootDocId = 'root-doc-id') } + this.res.sendStatus = code => { + this.EditorController.setRootDoc + .calledWith(this.project_id, this.rootDocId) + .should.equal(true) + code.should.equal(204) + return done() + } + return this.ProjectController.updateProjectSettings(this.req, this.res) + }) + }) + + describe('updateProjectAdminSettings', () => + it('should update the public access level', function(done) { + this.EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) + this.req.body = { + publicAccessLevel: (this.publicAccessLevel = 'readonly') + } + this.res.sendStatus = code => { + this.EditorController.setPublicAccessLevel + .calledWith(this.project_id, this.publicAccessLevel) + .should.equal(true) + code.should.equal(204) + return done() + } + return this.ProjectController.updateProjectAdminSettings( + this.req, + this.res + ) + })) + + describe('deleteProject', function() { + it('should tell the project deleter to archive when forever=false', function(done) { + this.res.sendStatus = code => { + this.ProjectDeleter.archiveProject + .calledWith(this.project_id) + .should.equal(true) + code.should.equal(200) + return done() + } + return this.ProjectController.deleteProject(this.req, this.res) + }) + + return it('should tell the project deleter to delete when forever=true', function(done) { + this.req.query = { forever: 'true' } + this.res.sendStatus = code => { + this.ProjectDeleter.deleteProject + .calledWith(this.project_id, { + deleterUser: this.user, + ipAddress: this.req.ip + }) + .should.equal(true) + code.should.equal(200) + return done() + } + return this.ProjectController.deleteProject(this.req, this.res) + }) + }) + + describe('restoreProject', () => + it('should tell the project deleter', function(done) { + this.res.sendStatus = code => { + this.ProjectDeleter.restoreProject + .calledWith(this.project_id) + .should.equal(true) + code.should.equal(200) + return done() + } + return this.ProjectController.restoreProject(this.req, this.res) + })) + + describe('cloneProject', () => + it('should call the project duplicator', function(done) { + this.res.send = json => { + this.ProjectDuplicator.duplicate + .calledWith(this.user, this.project_id, this.projectName) + .should.equal(true) + json.project_id.should.equal(this.project_id) + return done() + } + return this.ProjectController.cloneProject(this.req, this.res) + })) + + describe('newProject', function() { + it('should call the projectCreationHandler with createExampleProject', function(done) { + this.req.body.template = 'example' + this.res.send = json => { + this.ProjectCreationHandler.createExampleProject + .calledWith(this.user._id, this.projectName) + .should.equal(true) + this.ProjectCreationHandler.createBasicProject.called.should.equal( + false + ) + return done() + } + return this.ProjectController.newProject(this.req, this.res) + }) + + return it('should call the projectCreationHandler with createBasicProject', function(done) { + this.req.body.template = 'basic' + this.res.send = json => { + this.ProjectCreationHandler.createExampleProject.called.should.equal( + false + ) + this.ProjectCreationHandler.createBasicProject + .calledWith(this.user._id, this.projectName) + .should.equal(true) + return done() + } + return this.ProjectController.newProject(this.req, this.res) + }) + }) + + describe('projectListPage', function() { + beforeEach(function() { + this.tags = [ + { name: 1, project_ids: ['1', '2', '3'] }, + { name: 2, project_ids: ['a', '1'] }, + { name: 3, project_ids: ['a', 'b', 'c', 'd'] } + ] + this.notifications = [ + { _id: '1', user_id: '2', templateKey: '3', messageOpts: '4', key: '5' } + ] + this.projects = [ + { _id: 1, lastUpdated: 1, owner_ref: 'user-1' }, + { _id: 2, lastUpdated: 2, owner_ref: 'user-2', lastUpdatedBy: 'user-1' } + ] + this.collabertions = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] + this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] + this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] + this.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }] + this.allProjects = { + owned: this.projects, + readAndWrite: this.collabertions, + readOnly: this.readOnly, + tokenReadAndWrite: this.tokenReadAndWrite, + tokenReadOnly: this.tokenReadOnly + } + + this.users = { + 'user-1': { + first_name: 'James' + }, + 'user-2': { + first_name: 'Henry' + } + } + this.users[this.user._id] = this.user // Owner + this.UserModel.findById = (id, fields, callback) => { + return callback(null, this.users[id]) + } + this.UserGetter.getUserOrUserStubById = (id, fields, callback) => { + return callback(null, this.users[id]) + } + + this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false) + this.TagsHandler.getAllTags.callsArgWith(1, null, this.tags, {}) + this.NotificationsHandler.getUserNotifications = sinon + .stub() + .callsArgWith(1, null, this.notifications, {}) + this.ProjectGetter.findAllUsersProjects.callsArgWith( + 2, + null, + this.allProjects + ) + return this.Modules.hooks.fire + .withArgs('findAllV1Projects', this.user._id) + .yields(undefined) + }) // Without integration module hook, cb returns undefined + + it('should render the project/list page', function(done) { + this.res.render = (pageName, opts) => { + pageName.should.equal('project/list') + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should send the tags', function(done) { + this.res.render = (pageName, opts) => { + opts.tags.length.should.equal(this.tags.length) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should create trigger ip matcher notifications', function(done) { + this.settings.overleaf = true + this.res.render = (pageName, opts) => { + this.NotificationBuilder.ipMatcherAffiliation.called.should.equal(true) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should send the projects', function(done) { + this.res.render = (pageName, opts) => { + opts.projects.length.should.equal( + this.projects.length + + this.collabertions.length + + this.readOnly.length + + this.tokenReadAndWrite.length + + this.tokenReadOnly.length + ) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should send the user', function(done) { + this.res.render = (pageName, opts) => { + opts.user.should.deep.equal(this.user) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should inject the users', function(done) { + this.res.render = (pageName, opts) => { + opts.projects[0].owner.should.equal( + this.users[this.projects[0].owner_ref] + ) + opts.projects[1].owner.should.equal( + this.users[this.projects[1].owner_ref] + ) + opts.projects[1].lastUpdatedBy.should.equal( + this.users[this.projects[1].lastUpdatedBy] + ) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should send hasSubscription == false when no subscription', function(done) { + this.res.render = (pageName, opts) => { + opts.hasSubscription.should.equal(false) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should send hasSubscription == true when there is a subscription', function(done) { + this.LimitationsManager.hasPaidSubscription = sinon + .stub() + .callsArgWith(1, null, true) + this.res.render = (pageName, opts) => { + opts.hasSubscription.should.equal(true) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + describe('front widget', function(done) { + beforeEach(function() { + return (this.settings.overleaf = { + front_chat_widget_room_id: 'chat-room-id' + }) + }) + + it('should show for paid users', function(done) { + this.user.features.github = true + this.user.features.dropbox = true + this.res.render = (pageName, opts) => { + opts.frontChatWidgetRoomId.should.equal( + this.settings.overleaf.front_chat_widget_room_id + ) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should show for sample users', function(done) { + this.user._id = '588f3ddae8ebc1bac07c9f00' // last two digits + this.res.render = (pageName, opts) => { + opts.frontChatWidgetRoomId.should.equal( + this.settings.overleaf.front_chat_widget_room_id + ) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + return it('should not show for non sample users', function(done) { + this.user._id = '588f3ddae8ebc1bac07c9fff' // last two digits + this.res.render = (pageName, opts) => { + expect(opts.frontChatWidgetRoomId).to.equal(undefined) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + }) + + return describe('with overleaf-integration-web-module hook', function() { + beforeEach(function() { + this.V1Response = { + projects: [ + { + id: '123mockV1Id', + title: 'mock title', + updated_at: 1509616411, + removed: false, + archived: false + }, + { + id: '456mockV1Id', + title: 'mock title 2', + updated_at: 1509616411, + removed: true, + archived: false + } + ], + tags: [{ name: 'mock tag', project_ids: ['123mockV1Id'] }] + } + return this.Modules.hooks.fire + .withArgs('findAllV1Projects', this.user._id) + .yields(null, [this.V1Response]) + }) // Need to wrap response in array, as multiple hooks could fire + + it('should include V1 projects', function(done) { + this.res.render = (pageName, opts) => { + opts.projects.length.should.equal( + this.projects.length + + this.collabertions.length + + this.readOnly.length + + this.tokenReadAndWrite.length + + this.tokenReadOnly.length + + this.V1Response.projects.length + ) + opts.projects.forEach(function(p) { + // Check properties correctly mapped from V1 + expect(p).to.have.property('id') + expect(p).to.have.property('name') + expect(p).to.have.property('lastUpdated') + expect(p).to.have.property('accessLevel') + return expect(p).to.have.property('archived') + }) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + it('should include V1 tags', function(done) { + this.res.render = (pageName, opts) => { + opts.tags.length.should.equal( + this.tags.length + this.V1Response.tags.length + ) + opts.tags.forEach(function(t) { + expect(t).to.have.property('name') + return expect(t).to.have.property('project_ids') + }) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + return it('should have isShowingV1Projects flag', function(done) { + this.res.render = (pageName, opts) => { + opts.isShowingV1Projects.should.equal(true) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + }) + }) + + describe('projectListPage with duplicate projects', function() { + beforeEach(function() { + this.tags = [ + { name: 1, project_ids: ['1', '2', '3'] }, + { name: 2, project_ids: ['a', '1'] }, + { name: 3, project_ids: ['a', 'b', 'c', 'd'] } + ] + this.notifications = [ + { _id: '1', user_id: '2', templateKey: '3', messageOpts: '4', key: '5' } + ] + this.projects = [ + { _id: 1, lastUpdated: 1, owner_ref: 'user-1' }, + { _id: 2, lastUpdated: 2, owner_ref: 'user-2' } + ] + this.collabertions = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] + this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] + this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] + this.tokenReadOnly = [ + { _id: 6, lastUpdated: 5, owner_ref: 'user-4' }, // Also in tokenReadAndWrite + { _id: 7, lastUpdated: 4, owner_ref: 'user-5' } + ] + this.allProjects = { + owned: this.projects, + readAndWrite: this.collabertions, + readOnly: this.readOnly, + tokenReadAndWrite: this.tokenReadAndWrite, + tokenReadOnly: this.tokenReadOnly + } + + this.users = { + 'user-1': { + first_name: 'James' + }, + 'user-2': { + first_name: 'Henry' + } + } + this.users[this.user._id] = this.user // Owner + this.UserModel.findById = (id, fields, callback) => { + return callback(null, this.users[id]) + } + + this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false) + this.TagsHandler.getAllTags.callsArgWith(1, null, this.tags, {}) + this.NotificationsHandler.getUserNotifications = sinon + .stub() + .callsArgWith(1, null, this.notifications, {}) + this.ProjectGetter.findAllUsersProjects.callsArgWith( + 2, + null, + this.allProjects + ) + return this.Modules.hooks.fire + .withArgs('findAllV1Projects', this.user._id) + .yields(undefined) + }) // Without integration module hook, cb returns undefined + + it('should render the project/list page', function(done) { + this.res.render = (pageName, opts) => { + pageName.should.equal('project/list') + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + + return it('should omit one of the projects', function(done) { + this.res.render = (pageName, opts) => { + opts.projects.length.should.equal( + this.projects.length + + this.collabertions.length + + this.readOnly.length + + this.tokenReadAndWrite.length + + this.tokenReadOnly.length - + 1 + ) + return done() + } + return this.ProjectController.projectListPage(this.req, this.res) + }) + }) + + describe('renameProject', function() { + beforeEach(function() { + this.newProjectName = 'my supper great new project' + return (this.req.body.newProjectName = this.newProjectName) + }) + + it('should call the editor controller', function(done) { + this.EditorController.renameProject.callsArgWith(2) + this.res.sendStatus = code => { + code.should.equal(200) + this.EditorController.renameProject + .calledWith(this.project_id, this.newProjectName) + .should.equal(true) + return done() + } + return this.ProjectController.renameProject(this.req, this.res) + }) + + return it('should send an error to next() if there is a problem', function(done) { + let error + this.EditorController.renameProject.callsArgWith( + 2, + (error = new Error('problem')) + ) + const next = e => { + e.should.equal(error) + return done() + } + return this.ProjectController.renameProject(this.req, this.res, next) + }) + }) + + describe('loadEditor', function() { + beforeEach(function() { + this.settings.editorIsOpen = true + this.project = { + name: 'my proj', + _id: '213123kjlkj', + owner_ref: '59fc84d5fbea77482d436e1b' + } + this.brandedProject = { + name: 'my branded proj', + _id: '3252332', + owner_ref: '59fc84d5fbea77482d436e1b', + brandVariationId: '12' + } + this.user = { + _id: '588f3ddae8ebc1bac07c9fa4', + ace: { + fontSize: 'massive', + theme: 'sexy' + }, + email: 'bob@bob.com' + } + this.ProjectGetter.getProject.callsArgWith(2, null, this.project) + this.UserModel.findById.callsArgWith(1, null, this.user) + this.SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) + this.AuthorizationManager.getPrivilegeLevelForProject.callsArgWith( + 3, + null, + 'owner' + ) + this.ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() + this.InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) + this.AnalyticsManager.getLastOccurrence.yields(null, { mock: 'event' }) + return this.ProjectUpdateHandler.markAsOpened.callsArgWith(1) + }) + + it('should render the project/editor page', function(done) { + this.res.render = (pageName, opts) => { + pageName.should.equal('project/editor') + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should add user', function(done) { + this.res.render = (pageName, opts) => { + opts.user.email.should.equal(this.user.email) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should add on userSettings', function(done) { + this.res.render = (pageName, opts) => { + opts.userSettings.fontSize.should.equal(this.user.ace.fontSize) + opts.userSettings.editorTheme.should.equal(this.user.ace.theme) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should render the closed page if the editor is closed', function(done) { + this.settings.editorIsOpen = false + this.res.render = (pageName, opts) => { + pageName.should.equal('general/closed') + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should not render the page if the project can not be accessed', function(done) { + this.AuthorizationManager.getPrivilegeLevelForProject = sinon + .stub() + .callsArgWith(3, null, null) + this.res.sendStatus = (resCode, opts) => { + resCode.should.equal(401) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should reactivateProjectIfRequired', function(done) { + this.res.render = (pageName, opts) => { + this.InactiveProjectManager.reactivateProjectIfRequired + .calledWith(this.project_id) + .should.equal(true) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should mark project as opened', function(done) { + this.res.render = (pageName, opts) => { + this.ProjectUpdateHandler.markAsOpened + .calledWith(this.project_id) + .should.equal(true) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should call the brand variations handler for branded projects', function(done) { + this.ProjectGetter.getProject.callsArgWith(2, null, this.brandedProject) + this.res.render = (pageName, opts) => { + this.BrandVariationsHandler.getBrandVariationById + .calledWith() + .should.equal(true) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + it('should not call the brand variations handler for unbranded projects', function(done) { + this.res.render = (pageName, opts) => { + this.BrandVariationsHandler.getBrandVariationById.called.should.equal( + false + ) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + + return it('should expose the brand variation details as locals for branded projects', function(done) { + this.ProjectGetter.getProject.callsArgWith(2, null, this.brandedProject) + this.res.render = (pageName, opts) => { + opts.brandVariation.should.deep.equal(this.brandVariationDetails) + return done() + } + return this.ProjectController.loadEditor(this.req, this.res) + }) + }) + + describe('userProjectsJson', function() { + beforeEach(function(done) { + const projects = [ + { + archived: true, + id: 'a', + name: 'A', + accessLevel: 'a', + somethingElse: 1 + }, + { + archived: false, + id: 'b', + name: 'B', + accessLevel: 'b', + somethingElse: 1 + }, + { + archived: false, + id: 'c', + name: 'C', + accessLevel: 'c', + somethingElse: 1 + }, + { + archived: false, + id: 'd', + name: 'D', + accessLevel: 'd', + somethingElse: 1 + } + ] + this.ProjectGetter.findAllUsersProjects = sinon + .stub() + .callsArgWith(2, null, []) + this.ProjectController._buildProjectList = sinon.stub().returns(projects) + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns('abc') + return done() + }) + + return it('should produce a list of projects', function(done) { + this.res.json = data => { + expect(data).to.deep.equal({ + projects: [ + { _id: 'b', name: 'B', accessLevel: 'b' }, + { _id: 'c', name: 'C', accessLevel: 'c' }, + { _id: 'd', name: 'D', accessLevel: 'd' } + ] + }) + return done() + } + return this.ProjectController.userProjectsJson( + this.req, + this.res, + this.next + ) + }) + }) + + describe('projectEntitiesJson', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns('abc') + this.req.params = { Project_id: 'abcd' } + this.project = { _id: 'abcd' } + this.docs = [ + { path: '/things/b.txt', doc: true }, + { path: '/main.tex', doc: true } + ] + this.files = [{ path: '/things/a.txt' }] + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(1, null, this.project) + return (this.ProjectEntityHandler.getAllEntitiesFromProject = sinon + .stub() + .callsArgWith(1, null, this.docs, this.files)) + }) + + return it('should produce a list of entities', function(done) { + this.res.json = data => { + expect(data).to.deep.equal({ + project_id: 'abcd', + entities: [ + { path: '/main.tex', type: 'doc' }, + { path: '/things/a.txt', type: 'file' }, + { path: '/things/b.txt', type: 'doc' } + ] + }) + expect(this.ProjectGetter.getProject.callCount).to.equal(1) + expect( + this.ProjectEntityHandler.getAllEntitiesFromProject.callCount + ).to.equal(1) + return done() + } + return this.ProjectController.projectEntitiesJson( + this.req, + this.res, + this.next + ) + }) + }) + + return describe('_isInPercentageRollout', function() { + before(function() { + return (this.ids = [ + '5a05cd7621f9fe22be131740', + '5a05cd7821f9fe22be131741', + '5a05cd7921f9fe22be131742', + '5a05cd7a21f9fe22be131743', + '5a05cd7b21f9fe22be131744', + '5a05cd7c21f9fe22be131745', + '5a05cd7d21f9fe22be131746', + '5a05cd7e21f9fe22be131747', + '5a05cd7f21f9fe22be131748', + '5a05cd8021f9fe22be131749', + '5a05cd8021f9fe22be13174a', + '5a05cd8121f9fe22be13174b', + '5a05cd8221f9fe22be13174c', + '5a05cd8221f9fe22be13174d', + '5a05cd8321f9fe22be13174e', + '5a05cd8321f9fe22be13174f', + '5a05cd8421f9fe22be131750', + '5a05cd8421f9fe22be131751', + '5a05cd8421f9fe22be131752', + '5a05cd8521f9fe22be131753' + ]) + }) + + return it('should produce the expected results', function() { + expect( + this.ids.map(i => { + return this.ProjectController._isInPercentageRollout('abcd', i, 50) + }) + ).to.deep.equal([ + false, + false, + false, + false, + false, + false, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + true, + false, + true + ]) + return expect( + this.ids.map(i => { + return this.ProjectController._isInPercentageRollout('efgh', i, 50) + }) + ).to.deep.equal([ + false, + false, + false, + false, + true, + false, + false, + true, + false, + false, + true, + true, + true, + false, + true, + false, + true, + true, + false, + false + ]) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectCreationHandlerTests.js b/services/web/test/unit/src/Project/ProjectCreationHandlerTests.js new file mode 100644 index 0000000000..d5389cbbee --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectCreationHandlerTests.js @@ -0,0 +1,558 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-path-concat, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const spies = require('chai-spies') +const chai = require('chai').use(spies) +const sinon = require('sinon') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Project/ProjectCreationHandler.js' +const SandboxedModule = require('sandboxed-module') +const Settings = require('settings-sharelatex') +const Path = require('path') +const _ = require('underscore') + +describe('ProjectCreationHandler', function() { + const ownerId = '4eecb1c1bffa66588e0000a1' + const projectName = 'project name goes here' + const project_id = '4eecaffcbffa66588e000008' + const docId = '4eecb17ebffa66588e00003f' + const rootFolderId = '234adfa3r2afe' + + beforeEach(function() { + let Folder, Project + this.ProjectModel = Project = (function() { + Project = class Project { + static initClass() { + this.prototype.save = sinon.stub().callsArg(0) + this.prototype.rootFolder = [ + { + _id: rootFolderId, + docs: [] + } + ] + } + constructor(options) { + if (options == null) { + options = {} + } + this._id = project_id + this.owner_ref = options.owner_ref + this.name = options.name + this.overleaf = { history: {} } + } + } + Project.initClass() + return Project + })() + this.FolderModel = Folder = class Folder { + constructor(options) { + ;({ name: this.name } = options) + } + } + this.ProjectEntityUpdateHandler = { + addDoc: sinon.stub().callsArgWith(5, null, { _id: docId }), + addFile: sinon.stub().callsArg(6), + setRootDoc: sinon.stub().callsArg(2) + } + this.ProjectDetailsHandler = { validateProjectName: sinon.stub().yields() } + this.HistoryManager = { initializeProject: sinon.stub().callsArg(0) } + + this.user = { + first_name: 'first name here', + last_name: 'last name here', + ace: { + spellCheckLanguage: 'de' + } + } + + this.User = { findById: sinon.stub().callsArgWith(2, null, this.user) } + this.callback = sinon.stub() + + this.Settings = { apis: { project_history: {} } } + + this.AnalyticsManager = { recordEvent: sinon.stub() } + + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: this.User + }, + '../../models/Project': { Project: this.ProjectModel }, + '../../models/Folder': { Folder: this.FolderModel }, + '../History/HistoryManager': this.HistoryManager, + './ProjectEntityUpdateHandler': this.ProjectEntityUpdateHandler, + './ProjectDetailsHandler': this.ProjectDetailsHandler, + 'settings-sharelatex': this.Settings, + '../Analytics/AnalyticsManager': this.AnalyticsManager, + 'logger-sharelatex': { log() {} }, + 'metrics-sharelatex': { + inc() {}, + timeAsyncMethod() {} + } + } + })) + }) + + describe('Creating a Blank project', function() { + beforeEach(function() { + this.overleaf_id = 1234 + this.HistoryManager.initializeProject = sinon + .stub() + .callsArgWith(0, null, { overleaf_id: this.overleaf_id }) + return (this.ProjectModel.prototype.save = sinon.stub().callsArg(0)) + }) + + describe('successfully', function() { + it('should save the project', function(done) { + return this.handler.createBlankProject(ownerId, projectName, () => { + this.ProjectModel.prototype.save.called.should.equal(true) + return done() + }) + }) + + it('should return the project in the callback', function(done) { + return this.handler.createBlankProject(ownerId, projectName, function( + err, + project + ) { + project.name.should.equal(projectName) + ;(project.owner_ref + '').should.equal(ownerId) + return done() + }) + }) + + it('should initialize the project overleaf if history id not provided', function(done) { + this.handler.createBlankProject(ownerId, projectName, done) + return this.HistoryManager.initializeProject + .calledWith() + .should.equal(true) + }) + + it('should set the overleaf id if overleaf id not provided', function(done) { + return this.handler.createBlankProject( + ownerId, + projectName, + (err, project) => { + project.overleaf.history.id.should.equal(this.overleaf_id) + return done() + } + ) + }) + + it('should set the overleaf id if overleaf id provided', function(done) { + const overleaf_id = 2345 + const attributes = { + overleaf: { + history: { + id: overleaf_id + } + } + } + return this.handler.createBlankProject( + ownerId, + projectName, + attributes, + function(err, project) { + project.overleaf.history.id.should.equal(overleaf_id) + return done() + } + ) + }) + + it('should set the language from the user', function(done) { + return this.handler.createBlankProject(ownerId, projectName, function( + err, + project + ) { + project.spellCheckLanguage.should.equal('de') + return done() + }) + }) + + it('should set the imageName to currentImageName if set and no imageName attribute', function(done) { + this.Settings.currentImageName = 'mock-image-name' + return this.handler.createBlankProject( + ownerId, + projectName, + (err, project) => { + project.imageName.should.equal(this.Settings.currentImageName) + return done() + } + ) + }) + + it('should not set the imageName if no currentImageName', function(done) { + this.Settings.currentImageName = null + return this.handler.createBlankProject( + ownerId, + projectName, + (err, project) => { + expect(project.imageName).to.not.exist + return done() + } + ) + }) + + it('should set the imageName to the attribute value if set and not overwrite it with the currentImageName', function(done) { + this.Settings.currentImageName = 'mock-image-name' + const attributes = { imageName: 'attribute-image-name' } + return this.handler.createBlankProject( + ownerId, + projectName, + attributes, + (err, project) => { + project.imageName.should.equal(attributes.imageName) + return done() + } + ) + }) + + it('should not set the overleaf.history.display if not configured in settings', function(done) { + this.Settings.apis.project_history.displayHistoryForNewProjects = false + return this.handler.createBlankProject( + ownerId, + projectName, + (err, project) => { + expect(project.overleaf.history.display).to.not.exist + return done() + } + ) + }) + + it('should set the overleaf.history.display if configured in settings', function(done) { + this.Settings.apis.project_history.displayHistoryForNewProjects = true + return this.handler.createBlankProject( + ownerId, + projectName, + (err, project) => { + expect(project.overleaf.history.display).to.equal(true) + return done() + } + ) + }) + + it('should send a project-created event to analytics', function(done) { + return this.handler.createBlankProject( + ownerId, + projectName, + (err, project) => { + expect(this.AnalyticsManager.recordEvent.callCount).to.equal(1) + expect( + this.AnalyticsManager.recordEvent.calledWith( + ownerId, + 'project-created' + ) + ).to.equal(true) + return done() + } + ) + }) + + return it('should send a project-imported event when importing a project', function(done) { + return this.handler.createBlankProject( + ownerId, + projectName, + 1234, + (err, project) => { + expect(this.AnalyticsManager.recordEvent.callCount).to.equal(1) + expect( + this.AnalyticsManager.recordEvent.calledWith( + ownerId, + 'project-imported' + ) + ).to.equal(true) + return done() + } + ) + }) + }) + + describe('with an error', function() { + beforeEach(function() { + this.ProjectModel.prototype.save = sinon + .stub() + .callsArgWith(0, new Error('something went wrong')) + return this.handler.createBlankProject( + ownerId, + projectName, + this.callback + ) + }) + + return it('should return the error to the callback', function() { + return should.exist(this.callback.args[0][0]) + }) + }) + + return describe('with an invalid name', function() { + beforeEach(function() { + this.ProjectDetailsHandler.validateProjectName = sinon + .stub() + .yields(new Error('bad name')) + return this.handler.createBlankProject( + ownerId, + projectName, + this.callback + ) + }) + + it('should return the error to the callback', function() { + return should.exist(this.callback.args[0][0]) + }) + + return it('should not try to create the project', function() { + return this.ProjectModel.prototype.save.called.should.equal(false) + }) + }) + }) + + describe('Creating a basic project', function() { + beforeEach(function() { + this.project = new this.ProjectModel() + this.handler._buildTemplate = function( + template_name, + user, + project_name, + callback + ) { + if (template_name === 'mainbasic.tex') { + return callback(null, ['mainbasic.tex', 'lines']) + } + throw new Error(`unknown template: ${template_name}`) + } + sinon.spy(this.handler, '_buildTemplate') + this.handler.createBlankProject = sinon + .stub() + .callsArgWith(2, null, this.project) + this.handler._createRootDoc = sinon + .stub() + .callsArgWith(3, null, this.project) + return this.handler.createBasicProject( + ownerId, + projectName, + this.callback + ) + }) + + it('should create a blank project first', function() { + return this.handler.createBlankProject + .calledWith(ownerId, projectName) + .should.equal(true) + }) + + it('should create the root document', function() { + return this.handler._createRootDoc + .calledWith(this.project, ownerId, ['mainbasic.tex', 'lines']) + .should.equal(true) + }) + + return it('should build the mainbasic.tex template', function() { + return this.handler._buildTemplate + .calledWith('mainbasic.tex', ownerId, projectName) + .should.equal(true) + }) + }) + + describe('Creating a project from a snippet', function() { + beforeEach(function() { + this.project = new this.ProjectModel() + this.handler.createBlankProject = sinon + .stub() + .callsArgWith(2, null, this.project) + this.handler._createRootDoc = sinon + .stub() + .callsArgWith(3, null, this.project) + return this.handler.createProjectFromSnippet( + ownerId, + projectName, + ['snippet line 1', 'snippet line 2'], + this.callback + ) + }) + + it('should create a blank project first', function() { + return this.handler.createBlankProject + .calledWith(ownerId, projectName) + .should.equal(true) + }) + + return it('should create the root document', function() { + return this.handler._createRootDoc + .calledWith(this.project, ownerId, ['snippet line 1', 'snippet line 2']) + .should.equal(true) + }) + }) + + describe('Creating an example project', function() { + beforeEach(function() { + this.project = new this.ProjectModel() + this.handler._buildTemplate = function( + template_name, + user, + project_name, + callback + ) { + if (template_name === 'main.tex') { + return callback(null, ['main.tex', 'lines']) + } + if (template_name === 'references.bib') { + return callback(null, ['references.bib', 'lines']) + } + throw new Error(`unknown template: ${template_name}`) + } + sinon.spy(this.handler, '_buildTemplate') + this.handler.createBlankProject = sinon + .stub() + .callsArgWith(2, null, this.project) + this.handler._createRootDoc = sinon + .stub() + .callsArgWith(3, null, this.project) + return this.handler.createExampleProject( + ownerId, + projectName, + this.callback + ) + }) + + it('should create a blank project first', function() { + return this.handler.createBlankProject + .calledWith(ownerId, projectName) + .should.equal(true) + }) + + it('should create the root document', function() { + return this.handler._createRootDoc + .calledWith(this.project, ownerId, ['main.tex', 'lines']) + .should.equal(true) + }) + + it('should insert references.bib', function() { + return this.ProjectEntityUpdateHandler.addDoc + .calledWith( + project_id, + rootFolderId, + 'references.bib', + ['references.bib', 'lines'], + ownerId + ) + .should.equal(true) + }) + + it('should insert universe.jpg', function() { + return this.ProjectEntityUpdateHandler.addFile + .calledWith( + project_id, + rootFolderId, + 'universe.jpg', + Path.resolve( + __dirname + '/../../../../app/templates/project_files/universe.jpg' + ), + null, + ownerId + ) + .should.equal(true) + }) + + it('should build the main.tex template', function() { + return this.handler._buildTemplate + .calledWith('main.tex', ownerId, projectName) + .should.equal(true) + }) + + return it('should build the references.bib template', function() { + return this.handler._buildTemplate + .calledWith('references.bib', ownerId, projectName) + .should.equal(true) + }) + }) + + describe('_buildTemplate', function() { + beforeEach(function(done) { + return this.handler._buildTemplate( + 'main.tex', + this.user_id, + projectName, + (err, templateLines) => { + this.template = templateLines.reduce( + (singleLine, line) => `${singleLine}\n${line}` + ) + return done() + } + ) + }) + + it('should insert the project name into the template', function(done) { + this.template.indexOf(projectName).should.not.equal(-1) + return done() + }) + + it('should insert the users name into the template', function(done) { + this.template.indexOf(this.user.first_name).should.not.equal(-1) + this.template.indexOf(this.user.last_name).should.not.equal(-1) + return done() + }) + + it('should not have undefined in the template', function(done) { + this.template.indexOf('undefined').should.equal(-1) + return done() + }) + + it('should not have any underscore brackets in the output', function(done) { + this.template.indexOf('{{').should.equal(-1) + this.template.indexOf('<%=').should.equal(-1) + return done() + }) + + return it('should put the year in', function(done) { + this.template.indexOf(new Date().getUTCFullYear()).should.not.equal(-1) + return done() + }) + }) + + return describe('_createRootDoc', function() { + beforeEach(function(done) { + this.project = new this.ProjectModel() + + return this.handler._createRootDoc( + this.project, + ownerId, + ['line 1', 'line 2'], + done + ) + }) + + it('should insert main.tex', function() { + return this.ProjectEntityUpdateHandler.addDoc + .calledWith( + project_id, + rootFolderId, + 'main.tex', + ['line 1', 'line 2'], + ownerId + ) + .should.equal(true) + }) + + return it('should set the main doc id', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(project_id, docId) + .should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectDeleterTests.js b/services/web/test/unit/src/Project/ProjectDeleterTests.js new file mode 100644 index 0000000000..cd97e19f00 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectDeleterTests.js @@ -0,0 +1,276 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, + no-useless-constructor, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const modulePath = '../../../../app/src/Features/Project/ProjectDeleter' +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') + +describe('ProjectDeleter', function() { + beforeEach(function() { + let DeletedProject + this.project_id = '12312' + this.project = { + _id: this.project_id, + rootFolder: [], + collaberator_refs: ['collab1', 'collab2'], + readOnly_refs: ['readOnly1', 'readOnly2'], + owner_ref: 'owner ref here', + remove: sinon.stub().callsArg(0) + } + + this.user = { + _id: '588f3ddae8ebc1bac07c9fa4', + first_name: 'bjkdsjfk', + features: {} + } + + this.Project = { + update: sinon.stub().callsArgWith(3), + remove: sinon.stub().callsArgWith(1), + findOne: sinon.stub().callsArgWith(1, null, this.project), + find: sinon.stub().callsArgWith(1, null, [this.project]), + applyToAllFilesRecursivly: sinon.stub() + } + this.DeletedProject = DeletedProject = (function() { + DeletedProject = class DeletedProject { + static initClass() { + this.prototype.save = sinon.stub().callsArgWith(0) + } + constructor() {} + } + DeletedProject.initClass() + return DeletedProject + })() + this.documentUpdaterHandler = { + flushProjectToMongoAndDelete: sinon.stub().callsArgWith(1) + } + this.editorController = { + notifyUsersProjectHasBeenDeletedOrRenamed: sinon.stub().callsArgWith(1) + } + this.TagsHandler = { + removeProjectFromAllTags: sinon.stub().callsArgWith(2) + } + this.CollaboratorsHandler = { + removeUserFromAllProjets: sinon.stub().yields(), + getMemberIds: sinon + .stub() + .withArgs(this.project_id) + .yields(null, ['member-id-1', 'member-id-2']) + } + return (this.deleter = SandboxedModule.require(modulePath, { + requires: { + '../Editor/EditorController': this.editorController, + '../../models/Project': { Project: this.Project }, + '../../models/DeletedProject': { DeletedProject: this.DeletedProject }, + '../DocumentUpdater/DocumentUpdaterHandler': this + .documentUpdaterHandler, + '../Tags/TagsHandler': this.TagsHandler, + '../FileStore/FileStoreHandler': (this.FileStoreHandler = {}), + '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + 'logger-sharelatex': { + log() {} + } + } + })) + }) + + describe('mark as deleted by external source', function() { + const project_id = 1234 + it('should update the project with the flag set to true', function(done) { + return this.deleter.markAsDeletedByExternalSource(project_id, () => { + const conditions = { _id: project_id } + const update = { deletedByExternalDataSource: true } + this.Project.update.calledWith(conditions, update).should.equal(true) + return done() + }) + }) + + return it('should tell the editor controler so users are notified', function(done) { + return this.deleter.markAsDeletedByExternalSource(project_id, () => { + this.editorController.notifyUsersProjectHasBeenDeletedOrRenamed + .calledWith(project_id) + .should.equal(true) + return done() + }) + }) + }) + + describe('unmarkAsDeletedByExternalSource', function() { + beforeEach(function() { + this.Project.update = sinon.stub().callsArg(3) + this.callback = sinon.stub() + this.project = { + _id: this.project_id + } + return this.deleter.unmarkAsDeletedByExternalSource( + this.project_id, + this.callback + ) + }) + + return it('should remove the flag from the project', function() { + return this.Project.update + .calledWith( + { _id: this.project_id }, + { deletedByExternalDataSource: false } + ) + .should.equal(true) + }) + }) + + describe('deleteUsersProjects', function() { + beforeEach(function() { + return (this.deleter.deleteProject = sinon.stub().callsArg(1)) + }) + + it('should find all the projects owned by the user_id', function(done) { + return this.deleter.deleteUsersProjects(this.user._id, () => { + sinon.assert.calledWith(this.Project.find, { owner_ref: this.user._id }) + return done() + }) + }) + + it('should call deleteProject on the found projects', function(done) { + return this.deleter.deleteUsersProjects(this.user._id, () => { + sinon.assert.calledWith(this.deleter.deleteProject, this.project._id) + return done() + }) + }) + + it('should call deleteProject once for each project', function(done) { + this.Project.find.callsArgWith(1, null, [ + { _id: 'potato' }, + { _id: 'wombat' } + ]) + return this.deleter.deleteUsersProjects(this.user._id, () => { + sinon.assert.calledTwice(this.deleter.deleteProject) + sinon.assert.calledWith(this.deleter.deleteProject, 'wombat') + sinon.assert.calledWith(this.deleter.deleteProject, 'potato') + return done() + }) + }) + + return it('should remove all the projects the user is a collaborator of', function(done) { + return this.deleter.deleteUsersProjects(this.user._id, () => { + this.CollaboratorsHandler.removeUserFromAllProjets + .calledWith(this.user._id) + .should.equal(true) + return done() + }) + }) + }) + + describe('deleteProject', function() { + beforeEach(function() { + this.project_id = 'mock-project-id-123' + return (this.ip = '192.170.18.1') + }) + + it('should save a DeletedProject with additional deleterData', function(done) { + return this.deleter.deleteProject( + this.project_id, + { deleterUser: this.user, ipAddress: this.ip }, + (err, deletedProject) => { + this.DeletedProject.prototype.save.called.should.equal(true) + deletedProject.deleterData.deleterIpAddress.should.equal(this.ip) + deletedProject.deleterData.deleterId.should.equal(this.user._id) + return done() + } + ) + }) + + it('should flushProjectToMongoAndDelete in doc updater', function(done) { + return this.deleter.deleteProject( + this.project_id, + { deleterUser: this.user, ipAddress: this.ip }, + () => { + this.documentUpdaterHandler.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(true) + return done() + } + ) + }) + + it('should removeProjectFromAllTags', function(done) { + return this.deleter.deleteProject(this.project_id, () => { + this.TagsHandler.removeProjectFromAllTags + .calledWith('member-id-1', this.project_id) + .should.equal(true) + this.TagsHandler.removeProjectFromAllTags + .calledWith('member-id-2', this.project_id) + .should.equal(true) + return done() + }) + }) + + return it('should remove the project from Mongo', function(done) { + return this.deleter.deleteProject(this.project_id, () => { + this.Project.remove + .calledWith({ + _id: this.project_id + }) + .should.equal(true) + return done() + }) + }) + }) + + describe('archiveProject', function() { + beforeEach(function() { + return this.Project.update.callsArgWith(2) + }) + + return it('should update the project', function(done) { + return this.deleter.archiveProject(this.project_id, () => { + this.Project.update + .calledWith( + { + _id: this.project_id + }, + { + $set: { archived: true } + } + ) + .should.equal(true) + return done() + }) + }) + }) + + return describe('restoreProject', function() { + beforeEach(function() { + return this.Project.update.callsArgWith(2) + }) + + return it('should unset the archive attribute', function(done) { + return this.deleter.restoreProject(this.project_id, () => { + this.Project.update + .calledWith( + { + _id: this.project_id + }, + { + $unset: { archived: true } + } + ) + .should.equal(true) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectDetailsHandlerTests.js b/services/web/test/unit/src/Project/ProjectDetailsHandlerTests.js new file mode 100644 index 0000000000..4658038d74 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectDetailsHandlerTests.js @@ -0,0 +1,747 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const modulePath = '../../../../app/src/Features/Project/ProjectDetailsHandler' +const Errors = require('../../../../app/src/Features/Errors/Errors') +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const { assert } = require('chai') +const { expect } = require('chai') +require('chai').should() + +describe('ProjectDetailsHandler', function() { + beforeEach(function() { + this.project_id = '321l3j1kjkjl' + this.user_id = 'user-id-123' + this.project = { + name: 'project', + description: 'this is a great project', + something: 'should not exist', + compiler: 'latexxxxxx', + owner_ref: this.user_id + } + this.user = { features: 'mock-features' } + this.ProjectGetter = { + getProjectWithoutDocLines: sinon + .stub() + .callsArgWith(1, null, this.project), + getProject: sinon.stub().callsArgWith(2, null, this.project) + } + this.ProjectModel = { + update: sinon.stub(), + findOne: sinon.stub() + } + this.UserGetter = { getUser: sinon.stub().callsArgWith(1, null, this.user) } + this.tpdsUpdateSender = { moveEntity: sinon.stub().callsArgWith(1) } + this.ProjectEntityHandler = { + flushProjectToThirdPartyDataStore: sinon.stub().callsArg(1) + } + this.CollaboratorsHandler = { + removeUserFromProject: sinon.stub().callsArg(2) + } + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + './ProjectGetter': this.ProjectGetter, + '../../models/Project': { + Project: this.ProjectModel + }, + '../User/UserGetter': this.UserGetter, + '../ThirdPartyDataStore/TpdsUpdateSender': this.tpdsUpdateSender, + './ProjectEntityHandler': this.ProjectEntityHandler, + '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + 'logger-sharelatex': { + log() {}, + err() {} + }, + './ProjectTokenGenerator': (this.ProjectTokenGenerator = {}), + 'settings-sharelatex': (this.settings = { + defaultFeatures: 'default-features' + }) + } + })) + }) + + describe('getDetails', function() { + it('should find the project and owner', function(done) { + return this.handler.getDetails(this.project_id, (err, details) => { + details.name.should.equal(this.project.name) + details.description.should.equal(this.project.description) + details.compiler.should.equal(this.project.compiler) + details.features.should.equal(this.user.features) + assert.equal(details.something, undefined) + return done() + }) + }) + + it('should find overleaf metadata if it exists', function(done) { + this.project.overleaf = { id: 'id' } + return this.handler.getDetails(this.project_id, (err, details) => { + details.overleaf.should.equal(this.project.overleaf) + assert.equal(details.something, undefined) + return done() + }) + }) + + it('should return an error for a non-existent project', function(done) { + this.ProjectGetter.getProject.callsArg(2, null, null) + const err = new Errors.NotFoundError('project not found') + return this.handler.getDetails( + '0123456789012345678901234', + (error, details) => { + err.should.eql(error) + return done() + } + ) + }) + + it('should return the default features if no owner found', function(done) { + this.UserGetter.getUser.callsArgWith(1, null, null) + return this.handler.getDetails(this.project_id, (err, details) => { + details.features.should.equal(this.settings.defaultFeatures) + return done() + }) + }) + + return it('should return the error', function(done) { + const error = 'some error' + this.ProjectGetter.getProject.callsArgWith(2, error) + return this.handler.getDetails(this.project_id, err => { + err.should.equal(error) + return done() + }) + }) + }) + + describe('transferOwnership', function() { + beforeEach(function() { + this.handler.generateUniqueName = sinon + .stub() + .callsArgWith(2, null, 'teapot') + return this.ProjectModel.update.callsArgWith(2) + }) + + it("should return a not found error if the project can't be found", function(done) { + this.ProjectGetter.getProject.callsArgWith(2) + return this.handler.transferOwnership('abc', '123', function(err) { + err.should.exist + err.name.should.equal('NotFoundError') + return done() + }) + }) + + it("should return a not found error if the user can't be found", function(done) { + this.ProjectGetter.getProject.callsArgWith(2) + return this.handler.transferOwnership('abc', '123', function(err) { + err.should.exist + err.name.should.equal('NotFoundError') + return done() + }) + }) + + it('should return an error if user cannot be removed as collaborator ', function(done) { + const errorMessage = 'user-cannot-be-removed' + this.CollaboratorsHandler.removeUserFromProject.callsArgWith( + 2, + errorMessage + ) + return this.handler.transferOwnership('abc', '123', function(err) { + err.should.exist + err.should.equal(errorMessage) + return done() + }) + }) + + it('should transfer ownership of the project', function(done) { + return this.handler.transferOwnership('abc', '123', () => { + sinon.assert.calledWith( + this.ProjectModel.update, + { _id: 'abc' }, + sinon.match({ $set: { name: 'teapot' } }) + ) + return done() + }) + }) + + it("should remove the user from the project's collaborators", function(done) { + return this.handler.transferOwnership('abc', '123', () => { + sinon.assert.calledWith( + this.CollaboratorsHandler.removeUserFromProject, + 'abc', + '123' + ) + return done() + }) + }) + + it('should flush the project to tpds', function(done) { + return this.handler.transferOwnership('abc', '123', () => { + sinon.assert.calledWith( + this.ProjectEntityHandler.flushProjectToThirdPartyDataStore, + 'abc' + ) + return done() + }) + }) + + it('should generate a unique name for the project', function(done) { + return this.handler.transferOwnership('abc', '123', () => { + sinon.assert.calledWith( + this.handler.generateUniqueName, + '123', + this.project.name + ) + return done() + }) + }) + + return it('should append the supplied suffix to the project name, if passed', function(done) { + return this.handler.transferOwnership('abc', '123', ' wombat', () => { + sinon.assert.calledWith( + this.handler.generateUniqueName, + '123', + `${this.project.name} wombat` + ) + return done() + }) + }) + }) + + describe('getProjectDescription', function() { + it('should make a call to mongo just for the description', function(done) { + this.ProjectGetter.getProject.callsArgWith(2) + return this.handler.getProjectDescription( + this.project_id, + (err, description) => { + this.ProjectGetter.getProject + .calledWith(this.project_id, { description: true }) + .should.equal(true) + return done() + } + ) + }) + + return it('should return what the mongo call returns', function(done) { + const err = 'error' + const description = 'cool project' + this.ProjectGetter.getProject.callsArgWith(2, err, { description }) + return this.handler.getProjectDescription( + this.project_id, + (returnedErr, returnedDescription) => { + err.should.equal(returnedErr) + description.should.equal(returnedDescription) + return done() + } + ) + }) + }) + + describe('setProjectDescription', function() { + beforeEach(function() { + return (this.description = 'updated teh description') + }) + + return it('should update the project detials', function(done) { + this.ProjectModel.update.callsArgWith(2) + return this.handler.setProjectDescription( + this.project_id, + this.description, + () => { + this.ProjectModel.update + .calledWith( + { _id: this.project_id }, + { description: this.description } + ) + .should.equal(true) + return done() + } + ) + }) + }) + + describe('renameProject', function() { + beforeEach(function() { + this.handler.validateProjectName = sinon.stub().yields() + this.ProjectModel.update.callsArgWith(2) + return (this.newName = 'new name here') + }) + + it('should update the project with the new name', function(done) { + const newName = 'new name here' + return this.handler.renameProject(this.project_id, this.newName, () => { + this.ProjectModel.update + .calledWith({ _id: this.project_id }, { name: this.newName }) + .should.equal(true) + return done() + }) + }) + + it('should tell the tpdsUpdateSender', function(done) { + return this.handler.renameProject(this.project_id, this.newName, () => { + this.tpdsUpdateSender.moveEntity + .calledWith({ + project_id: this.project_id, + project_name: this.project.name, + newProjectName: this.newName + }) + .should.equal(true) + return done() + }) + }) + + return it('should not do anything with an invalid name', function(done) { + this.handler.validateProjectName = sinon + .stub() + .yields(new Error('invalid name')) + return this.handler.renameProject(this.project_id, this.newName, () => { + this.tpdsUpdateSender.moveEntity.called.should.equal(false) + this.ProjectModel.update.called.should.equal(false) + return done() + }) + }) + }) + + describe('validateProjectName', function() { + it('should reject undefined names', function(done) { + return this.handler.validateProjectName(undefined, function(error) { + expect(error).to.exist + return done() + }) + }) + + it('should reject empty names', function(done) { + return this.handler.validateProjectName('', function(error) { + expect(error).to.exist + return done() + }) + }) + + it('should reject names with /s', function(done) { + return this.handler.validateProjectName('foo/bar', function(error) { + expect(error).to.exist + return done() + }) + }) + + it('should reject names with \\s', function(done) { + return this.handler.validateProjectName('foo\\bar', function(error) { + expect(error).to.exist + return done() + }) + }) + + it('should reject long names', function(done) { + return this.handler.validateProjectName( + new Array(1000).join('a'), + function(error) { + expect(error).to.exist + return done() + } + ) + }) + + return it('should accept normal names', function(done) { + return this.handler.validateProjectName('foobar', function(error) { + expect(error).to.not.exist + return done() + }) + }) + }) + + describe('ensureProjectNameIsUnique', function() { + beforeEach(function() { + this.result = { + owned: [ + { _id: 1, name: 'name' }, + { _id: 2, name: 'name1' }, + { _id: 3, name: 'name11' }, + { _id: 100, name: 'numeric' } + ], + readAndWrite: [{ _id: 4, name: 'name2' }, { _id: 5, name: 'name22' }], + readOnly: [{ _id: 6, name: 'name3' }, { _id: 7, name: 'name33' }], + tokenReadAndWrite: [ + { _id: 8, name: 'name4' }, + { _id: 9, name: 'name44' } + ], + tokenReadOnly: [ + { _id: 10, name: 'name5' }, + { _id: 11, name: 'name55' }, + { _id: 12, name: 'x'.repeat(15) } + ] + } + for (let i of Array.from( + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ].concat([30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]) + )) { + this.result.owned.push({ _id: 100 + i, name: `numeric (${i})` }) + } + return (this.ProjectGetter.findAllUsersProjects = sinon + .stub() + .callsArgWith(2, null, this.result)) + }) + + it('should leave a unique name unchanged', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'unique-name', + ['-test-suffix'], + function(error, name, changed) { + expect(name).to.equal('unique-name') + expect(changed).to.equal(false) + return done() + } + ) + }) + + it('should append a suffix to an existing name', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'name1', + ['-test-suffix'], + function(error, name, changed) { + expect(name).to.equal('name1-test-suffix') + expect(changed).to.equal(true) + return done() + } + ) + }) + + it('should fallback to a second suffix when needed', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'name1', + ['1', '-test-suffix'], + function(error, name, changed) { + expect(name).to.equal('name1-test-suffix') + expect(changed).to.equal(true) + return done() + } + ) + }) + + it('should truncate the name when append a suffix if the result is too long', function(done) { + this.handler.MAX_PROJECT_NAME_LENGTH = 20 + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'x'.repeat(15), + ['-test-suffix'], + function(error, name, changed) { + expect(name).to.equal('x'.repeat(8) + '-test-suffix') + expect(changed).to.equal(true) + return done() + } + ) + }) + + it('should use a numeric index if no suffix is supplied', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'name1', + [], + function(error, name, changed) { + expect(name).to.equal('name1 (1)') + expect(changed).to.equal(true) + return done() + } + ) + }) + + it('should use a numeric index if all suffixes are exhausted', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'name', + ['1', '11'], + function(error, name, changed) { + expect(name).to.equal('name (1)') + expect(changed).to.equal(true) + return done() + } + ) + }) + + it('should find the next lowest available numeric index for the base name', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'numeric', + [], + function(error, name, changed) { + expect(name).to.equal('numeric (21)') + expect(changed).to.equal(true) + return done() + } + ) + }) + + it('should find the next available numeric index when a numeric index is already present', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'numeric (5)', + [], + function(error, name, changed) { + expect(name).to.equal('numeric (21)') + expect(changed).to.equal(true) + return done() + } + ) + }) + + return it('should not find a numeric index lower than the one already present', function(done) { + return this.handler.ensureProjectNameIsUnique( + this.user_id, + 'numeric (31)', + [], + function(error, name, changed) { + expect(name).to.equal('numeric (41)') + expect(changed).to.equal(true) + return done() + } + ) + }) + }) + + describe('fixProjectName', function() { + it('should change empty names to Untitled', function() { + return expect(this.handler.fixProjectName('')).to.equal('Untitled') + }) + + it('should replace / with -', function() { + return expect(this.handler.fixProjectName('foo/bar')).to.equal('foo-bar') + }) + + it("should replace \\ with ''", function() { + return expect(this.handler.fixProjectName('foo \\ bar')).to.equal( + 'foo bar' + ) + }) + + it('should truncate long names', function() { + return expect( + this.handler.fixProjectName(new Array(1000).join('a')) + ).to.equal('a'.repeat(150)) + }) + + return it('should accept normal names', function() { + return expect(this.handler.fixProjectName('foobar')).to.equal('foobar') + }) + }) + + describe('setPublicAccessLevel', function() { + beforeEach(function() { + this.ProjectModel.update.callsArgWith(2) + return (this.accessLevel = 'readOnly') + }) + + it('should update the project with the new level', function(done) { + return this.handler.setPublicAccessLevel( + this.project_id, + this.accessLevel, + () => { + this.ProjectModel.update + .calledWith( + { _id: this.project_id }, + { publicAccesLevel: this.accessLevel } + ) + .should.equal(true) + return done() + } + ) + }) + + it('should not produce an error', function(done) { + return this.handler.setPublicAccessLevel( + this.project_id, + this.accessLevel, + err => { + expect(err).to.not.exist + return done() + } + ) + }) + + return describe('when update produces an error', function() { + beforeEach(function() { + return this.ProjectModel.update.callsArgWith(2, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.handler.setPublicAccessLevel( + this.project_id, + this.accessLevel, + err => { + expect(err).to.exist + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + }) + }) + + return describe('ensureTokensArePresent', function() { + beforeEach(function() {}) + + describe('when the project has tokens', function() { + beforeEach(function() { + this.project = { + _id: this.project_id, + tokens: { + readOnly: 'aaa', + readAndWrite: '42bbb', + readAndWritePrefix: '42' + } + } + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project) + return (this.ProjectModel.update = sinon.stub()) + }) + + it('should get the project', function(done) { + return this.handler.ensureTokensArePresent( + this.project_id, + (err, tokens) => { + expect(this.ProjectGetter.getProject.callCount).to.equal(1) + expect( + this.ProjectGetter.getProject.calledWith(this.project_id, { + tokens: 1 + }) + ).to.equal(true) + return done() + } + ) + }) + + it('should not update the project with new tokens', function(done) { + return this.handler.ensureTokensArePresent( + this.project_id, + (err, tokens) => { + expect(this.ProjectModel.update.callCount).to.equal(0) + return done() + } + ) + }) + + return it('should produce the tokens without error', function(done) { + return this.handler.ensureTokensArePresent( + this.project_id, + (err, tokens) => { + expect(err).to.not.exist + expect(tokens).to.deep.equal(this.project.tokens) + return done() + } + ) + }) + }) + + return describe('when tokens are missing', function() { + beforeEach(function() { + this.project = { _id: this.project_id } + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project) + this.readOnlyToken = 'abc' + this.readAndWriteToken = '42def' + this.readAndWriteTokenPrefix = '42' + this.ProjectTokenGenerator.generateUniqueReadOnlyToken = sinon + .stub() + .callsArgWith(0, null, this.readOnlyToken) + this.ProjectTokenGenerator.readAndWriteToken = sinon.stub().returns({ + token: this.readAndWriteToken, + numericPrefix: this.readAndWriteTokenPrefix + }) + return (this.ProjectModel.update = sinon.stub().callsArgWith(2, null)) + }) + + it('should get the project', function(done) { + return this.handler.ensureTokensArePresent( + this.project_id, + (err, tokens) => { + expect(this.ProjectGetter.getProject.callCount).to.equal(1) + expect( + this.ProjectGetter.getProject.calledWith(this.project_id, { + tokens: 1 + }) + ).to.equal(true) + return done() + } + ) + }) + + it('should update the project with new tokens', function(done) { + return this.handler.ensureTokensArePresent( + this.project_id, + (err, tokens) => { + expect( + this.ProjectTokenGenerator.generateUniqueReadOnlyToken.callCount + ).to.equal(1) + expect( + this.ProjectTokenGenerator.readAndWriteToken.callCount + ).to.equal(1) + expect(this.ProjectModel.update.callCount).to.equal(1) + expect( + this.ProjectModel.update.calledWith( + { _id: this.project_id }, + { + $set: { + tokens: { + readOnly: this.readOnlyToken, + readAndWrite: this.readAndWriteToken, + readAndWritePrefix: this.readAndWriteTokenPrefix + } + } + } + ) + ).to.equal(true) + return done() + } + ) + }) + + return it('should produce the tokens without error', function(done) { + return this.handler.ensureTokensArePresent( + this.project_id, + (err, tokens) => { + expect(err).to.not.exist + expect(tokens).to.deep.equal({ + readOnly: this.readOnlyToken, + readAndWrite: this.readAndWriteToken, + readAndWritePrefix: this.readAndWriteTokenPrefix + }) + return done() + } + ) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectDuplicatorTests.js b/services/web/test/unit/src/Project/ProjectDuplicatorTests.js new file mode 100644 index 0000000000..eb065eef10 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectDuplicatorTests.js @@ -0,0 +1,429 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai').should() +const modulePath = '../../../../app/src/Features/Project/ProjectDuplicator.js' +const SandboxedModule = require('sandboxed-module') + +describe('ProjectDuplicator', function() { + beforeEach(function() { + this.level2folder = { + name: 'level2folderName', + _id: 'level2folderId', + docs: [ + (this.doc2 = { _id: 'doc2_id', name: 'level2folderDocName' }), + undefined + ], + folders: [], + fileRefs: [{ name: 'file2', _id: 'file2' }] + } + this.level1folder = { + name: 'level1folder', + _id: 'level1folderId', + docs: [(this.doc1 = { _id: 'doc1_id', name: 'level1folderDocName' })], + folders: [this.level2folder], + fileRefs: [{ name: 'file1', _id: 'file1' }, null] // the null is intentional to test null docs/files + } + this.rootFolder = { + name: 'rootFolder', + _id: 'rootFolderId', + docs: [(this.doc0 = { _id: 'doc0_id', name: 'rootDocHere' })], + folders: [this.level1folder, {}], + fileRefs: [{ name: 'file0', _id: 'file0' }] + } + this.project = { + _id: (this.old_project_id = 'this_is_the_old_project_id'), + rootDoc_id: 'rootDoc_id', + rootFolder: [this.rootFolder], + compiler: 'this_is_a_Compiler' + } + + this.docContents = [ + { + _id: this.doc0._id, + lines: (this.doc0_lines = ['zero']) + }, + { + _id: this.doc1._id, + lines: (this.doc1_lines = ['one']) + }, + { + _id: this.doc2._id, + lines: (this.doc2_lines = ['two']) + } + ] + this.DocstoreManager = { + getAllDocs: sinon.stub().callsArgWith(1, null, this.docContents) + } + + this.owner = { _id: 'this_is_the_owner' } + this.stubbedNewProject = { + _id: (this.new_project_id = 'new_project_id'), + readOnly_refs: [], + collaberator_refs: [], + rootFolder: [{ _id: 'new_root_folder_id' }] + } + this.foundRootDoc = { _id: 'rootDocId', name: 'rootDocHere' } + + this.creationHandler = { + createBlankProject: sinon + .stub() + .callsArgWith(2, null, this.stubbedNewProject) + } + + this.newFolder = { _id: 'newFolderId' } + + this.locator = { + findRootDoc: sinon.stub().callsArgWith(1, null, this.foundRootDoc, {}) + } + + this.projectOptionsHandler = { setCompiler: sinon.stub().callsArg(2) } + this.ProjectEntityUpdateHandler = { + addDoc: sinon.stub().callsArgWith(5, null, { name: 'somDoc' }), + copyFileFromExistingProjectWithProject: sinon.stub(), + setRootDoc: sinon.stub(), + addFolder: sinon.stub().callsArgWith(3, null, this.newFolder) + } + + this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject + .withArgs( + sinon.match.any, + sinon.match.any, + sinon.match.any, + sinon.match.any, + 'BROKEN-FILE', + sinon.match.any, + sinon.match.any + ) + .callsArgWith(6, new Error('failed')) + this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject + .withArgs( + sinon.match.any, + sinon.match.any, + sinon.match.any, + sinon.match.any, + sinon.match.object, + sinon.match.any + ) + .callsArg(6) + this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject + .withArgs( + sinon.match.any, + sinon.match.any, + sinon.match.any, + sinon.match.any, + null, + sinon.match.any + ) + .callsArg(6) + + this.DocumentUpdaterHandler = { + flushProjectToMongo: sinon.stub().callsArg(1) + } + + this.Project = { + findById: sinon.stub().callsArgWith(1, null, this.project) + } + + this.ProjectGetter = { getProject: sinon.stub() } + + this.ProjectGetter.getProject + .withArgs(this.old_project_id, sinon.match.any) + .callsArgWith(2, null, this.project) + this.ProjectGetter.getProject + .withArgs(this.new_project_id, sinon.match.any) + .callsArgWith(2, null, this.stubbedNewProject) + + this.ProjectDeleter = { deleteProject: sinon.stub().callsArgWith(1, null) } + + return (this.duplicator = SandboxedModule.require(modulePath, { + requires: { + '../../models/Project': { Project: this.Project }, + '../DocumentUpdater/DocumentUpdaterHandler': this + .DocumentUpdaterHandler, + './ProjectCreationHandler': this.creationHandler, + './ProjectEntityUpdateHandler': this.ProjectEntityUpdateHandler, + './ProjectLocator': this.locator, + './ProjectDeleter': this.ProjectDeleter, + './ProjectOptionsHandler': this.projectOptionsHandler, + '../Docstore/DocstoreManager': this.DocstoreManager, + './ProjectGetter': this.ProjectGetter, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + describe('when the copy succeeds', function() { + it('should look up the original project', function(done) { + const newProjectName = 'someProj' + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + newProjectName, + (err, newProject) => { + this.ProjectGetter.getProject + .calledWith(this.old_project_id) + .should.equal(true) + return done() + } + ) + }) + + it('should flush the original project to mongo', function(done) { + const newProjectName = 'someProj' + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + newProjectName, + (err, newProject) => { + this.DocumentUpdaterHandler.flushProjectToMongo + .calledWith(this.old_project_id) + .should.equal(true) + return done() + } + ) + }) + + it('should create a blank project', function(done) { + const newProjectName = 'someProj' + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + newProjectName, + (err, newProject) => { + newProject._id.should.equal(this.stubbedNewProject._id) + this.creationHandler.createBlankProject + .calledWith(this.owner._id, newProjectName) + .should.equal(true) + return done() + } + ) + }) + + it('should use the same compiler', function(done) { + this.ProjectEntityUpdateHandler.addDoc.callsArgWith( + 5, + null, + this.rootFolder.docs[0], + this.owner._id + ) + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + this.projectOptionsHandler.setCompiler + .calledWith(this.stubbedNewProject._id, this.project.compiler) + .should.equal(true) + return done() + } + ) + }) + + it('should use the same root doc', function(done) { + this.ProjectEntityUpdateHandler.addDoc.callsArgWith( + 5, + null, + this.rootFolder.docs[0], + this.owner._id + ) + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.stubbedNewProject._id, this.rootFolder.docs[0]._id) + .should.equal(true) + return done() + } + ) + }) + + it('should not copy the collaberators or read only refs', function(done) { + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + newProject.collaberator_refs.length.should.equal(0) + newProject.readOnly_refs.length.should.equal(0) + return done() + } + ) + }) + + it('should copy all the folders', function(done) { + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + this.ProjectEntityUpdateHandler.addFolder + .calledWith( + this.new_project_id, + this.stubbedNewProject.rootFolder[0]._id, + this.level1folder.name + ) + .should.equal(true) + this.ProjectEntityUpdateHandler.addFolder + .calledWith( + this.new_project_id, + this.newFolder._id, + this.level2folder.name + ) + .should.equal(true) + this.ProjectEntityUpdateHandler.addFolder.callCount.should.equal(2) + return done() + } + ) + }) + + it('should copy all the docs', function(done) { + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + this.DocstoreManager.getAllDocs + .calledWith(this.old_project_id) + .should.equal(true) + this.ProjectEntityUpdateHandler.addDoc + .calledWith( + this.new_project_id, + this.stubbedNewProject.rootFolder[0]._id, + this.doc0.name, + this.doc0_lines, + this.owner._id + ) + .should.equal(true) + this.ProjectEntityUpdateHandler.addDoc + .calledWith( + this.new_project_id, + this.newFolder._id, + this.doc1.name, + this.doc1_lines, + this.owner._id + ) + .should.equal(true) + this.ProjectEntityUpdateHandler.addDoc + .calledWith( + this.new_project_id, + this.newFolder._id, + this.doc2.name, + this.doc2_lines, + this.owner._id + ) + .should.equal(true) + return done() + } + ) + }) + + return it('should copy all the files', function(done) { + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject + .calledWith( + this.stubbedNewProject._id, + this.stubbedNewProject, + this.stubbedNewProject.rootFolder[0]._id, + this.project._id, + this.rootFolder.fileRefs[0], + this.owner._id + ) + .should.equal(true) + this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject + .calledWith( + this.stubbedNewProject._id, + this.stubbedNewProject, + this.newFolder._id, + this.project._id, + this.level1folder.fileRefs[0], + this.owner._id + ) + .should.equal(true) + this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject + .calledWith( + this.stubbedNewProject._id, + this.stubbedNewProject, + this.newFolder._id, + this.project._id, + this.level2folder.fileRefs[0], + this.owner._id + ) + .should.equal(true) + return done() + } + ) + }) + }) + + return describe('when there is an error', function() { + beforeEach(function() { + return (this.rootFolder.fileRefs = [ + { name: 'file0', _id: 'file0' }, + 'BROKEN-FILE', + { name: 'file1', _id: 'file1' }, + { name: 'file2', _id: 'file2' } + ]) + }) + + it('should delete the broken cloned project', function(done) { + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + this.ProjectDeleter.deleteProject + .calledWith(this.stubbedNewProject._id) + .should.equal(true) + return done() + } + ) + }) + + it('should not delete the original project', function(done) { + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + this.ProjectDeleter.deleteProject + .calledWith(this.old_project_id) + .should.equal(false) + return done() + } + ) + }) + + return it('should return an error', function(done) { + return this.duplicator.duplicate( + this.owner, + this.old_project_id, + '', + (err, newProject) => { + err.should.not.equal(null) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectEditorHandlerTests.js b/services/web/test/unit/src/Project/ProjectEditorHandlerTests.js new file mode 100644 index 0000000000..8fe24d8871 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectEditorHandlerTests.js @@ -0,0 +1,361 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const { expect } = chai +const should = chai.should() + +const modulePath = '../../../../app/src/Features/Project/ProjectEditorHandler' +const SandboxedModule = require('sandboxed-module') + +describe('ProjectEditorHandler', function() { + beforeEach(function() { + this.project = { + _id: 'project-id', + name: 'Project Name', + rootDoc_id: 'file-id', + publicAccesLevel: 'private', + deletedByExternalDataSource: false, + rootFolder: [ + { + _id: 'root-folder-id', + name: '', + docs: [], + fileRefs: [], + folders: [ + { + _id: 'sub-folder-id', + name: 'folder', + docs: [ + { + _id: 'doc-id', + name: 'main.tex', + lines: (this.lines = ['line 1', 'line 2', 'line 3']) + } + ], + fileRefs: [ + { + _id: 'file-id', + name: 'image.png', + created: (this.created = new Date()), + size: 1234 + } + ], + folders: [] + } + ] + } + ], + deletedDocs: [ + { + _id: 'deleted-doc-id', + name: 'main.tex', + deletedAt: (this.deletedAt = new Date('2017-01-01')) + } + ] + } + this.members = [ + { + user: (this.owner = { + _id: 'owner-id', + first_name: 'Owner', + last_name: 'ShareLaTeX', + email: 'owner@sharelatex.com' + }), + privilegeLevel: 'owner' + }, + { + user: { + _id: 'read-only-id', + first_name: 'Read', + last_name: 'Only', + email: 'read-only@sharelatex.com' + }, + privilegeLevel: 'readOnly' + }, + { + user: { + _id: 'read-write-id', + first_name: 'Read', + last_name: 'Write', + email: 'read-write@sharelatex.com' + }, + privilegeLevel: 'readAndWrite' + } + ] + this.invites = [ + { + _id: 'invite_one', + email: 'user-one@example.com', + privileges: 'readOnly', + projectId: this.project._id + }, + { + _id: 'invite_two', + email: 'user-two@example.com', + privileges: 'readOnly', + projectId: this.project._id + } + ] + return (this.handler = SandboxedModule.require(modulePath)) + }) + + describe('buildProjectModelView', function() { + describe('with owner and members included', function() { + beforeEach(function() { + return (this.result = this.handler.buildProjectModelView( + this.project, + this.members, + this.invites + )) + }) + + it('should include the id', function() { + should.exist(this.result._id) + return this.result._id.should.equal('project-id') + }) + + it('should include the name', function() { + should.exist(this.result.name) + return this.result.name.should.equal('Project Name') + }) + + it('should include the root doc id', function() { + should.exist(this.result.rootDoc_id) + return this.result.rootDoc_id.should.equal('file-id') + }) + + it('should include the public access level', function() { + should.exist(this.result.publicAccesLevel) + return this.result.publicAccesLevel.should.equal('private') + }) + + it('should include the owner', function() { + should.exist(this.result.owner) + this.result.owner._id.should.equal('owner-id') + this.result.owner.email.should.equal('owner@sharelatex.com') + this.result.owner.first_name.should.equal('Owner') + this.result.owner.last_name.should.equal('ShareLaTeX') + return this.result.owner.privileges.should.equal('owner') + }) + + it('should include the deletedDocs', function() { + should.exist(this.result.deletedDocs) + return this.result.deletedDocs.should.equal(this.project.deletedDocs) + }) + + it('should gather readOnly_refs and collaberators_refs into a list of members', function() { + const findMember = id => { + for (let member of Array.from(this.result.members)) { + if (member._id === id) { + return member + } + } + return null + } + + this.result.members.length.should.equal(2) + + should.exist(findMember('read-only-id')) + findMember('read-only-id').privileges.should.equal('readOnly') + findMember('read-only-id').first_name.should.equal('Read') + findMember('read-only-id').last_name.should.equal('Only') + findMember('read-only-id').email.should.equal( + 'read-only@sharelatex.com' + ) + + should.exist(findMember('read-write-id')) + findMember('read-write-id').privileges.should.equal('readAndWrite') + findMember('read-write-id').first_name.should.equal('Read') + findMember('read-write-id').last_name.should.equal('Write') + return findMember('read-write-id').email.should.equal( + 'read-write@sharelatex.com' + ) + }) + + it('should include folders in the project', function() { + this.result.rootFolder[0]._id.should.equal('root-folder-id') + this.result.rootFolder[0].name.should.equal('') + + this.result.rootFolder[0].folders[0]._id.should.equal('sub-folder-id') + return this.result.rootFolder[0].folders[0].name.should.equal('folder') + }) + + it('should not duplicate folder contents', function() { + this.result.rootFolder[0].docs.length.should.equal(0) + return this.result.rootFolder[0].fileRefs.length.should.equal(0) + }) + + it('should include files in the project', function() { + this.result.rootFolder[0].folders[0].fileRefs[0]._id.should.equal( + 'file-id' + ) + this.result.rootFolder[0].folders[0].fileRefs[0].name.should.equal( + 'image.png' + ) + this.result.rootFolder[0].folders[0].fileRefs[0].created.should.equal( + this.created + ) + return should.not.exist( + this.result.rootFolder[0].folders[0].fileRefs[0].size + ) + }) + + it('should include docs in the project but not the lines', function() { + this.result.rootFolder[0].folders[0].docs[0]._id.should.equal('doc-id') + this.result.rootFolder[0].folders[0].docs[0].name.should.equal( + 'main.tex' + ) + return should.not.exist( + this.result.rootFolder[0].folders[0].docs[0].lines + ) + }) + + return it('should include invites', function() { + should.exist(this.result.invites) + return this.result.invites.should.deep.equal(this.invites) + }) + }) + + describe('deletedByExternalDataSource', function() { + it('should set the deletedByExternalDataSource flag to false when it is not there', function() { + delete this.project.deletedByExternalDataSource + const result = this.handler.buildProjectModelView( + this.project, + this.members, + [], + [] + ) + return result.deletedByExternalDataSource.should.equal(false) + }) + + it('should set the deletedByExternalDataSource flag to false when it is false', function() { + const result = this.handler.buildProjectModelView( + this.project, + this.members, + [], + [] + ) + return result.deletedByExternalDataSource.should.equal(false) + }) + + return it('should set the deletedByExternalDataSource flag to true when it is true', function() { + this.project.deletedByExternalDataSource = true + const result = this.handler.buildProjectModelView( + this.project, + this.members, + [], + [] + ) + return result.deletedByExternalDataSource.should.equal(true) + }) + }) + + return describe('features', function() { + beforeEach(function() { + this.owner.features = { + versioning: true, + collaborators: 3, + compileGroup: 'priority', + compileTimeout: 96 + } + return (this.result = this.handler.buildProjectModelView( + this.project, + this.members, + [], + [] + )) + }) + + return it('should copy the owner features to the project', function() { + this.result.features.versioning.should.equal( + this.owner.features.versioning + ) + this.result.features.collaborators.should.equal( + this.owner.features.collaborators + ) + this.result.features.compileGroup.should.equal( + this.owner.features.compileGroup + ) + return this.result.features.compileTimeout.should.equal( + this.owner.features.compileTimeout + ) + }) + }) + }) + + return describe('buildOwnerAndMembersViews', function() { + beforeEach(function() { + this.owner.features = { + versioning: true, + collaborators: 3, + compileGroup: 'priority', + compileTimeout: 22 + } + return (this.result = this.handler.buildOwnerAndMembersViews( + this.members + )) + }) + + it('should produce an object with the right keys', function() { + return expect(this.result).to.have.all.keys([ + 'owner', + 'ownerFeatures', + 'members' + ]) + }) + + it('should separate the owner from the members', function() { + this.result.members.length.should.equal(this.members.length - 1) + expect(this.result.owner._id).to.equal(this.owner._id) + expect(this.result.owner.email).to.equal(this.owner.email) + return expect( + this.result.members.filter(m => m._id === this.owner._id).length + ).to.equal(0) + }) + + it('should extract the ownerFeatures from the owner object', function() { + return expect(this.result.ownerFeatures).to.deep.equal( + this.owner.features + ) + }) + + return describe('when there is no owner', function() { + beforeEach(function() { + // remove the owner from members list + this.membersWithoutOwner = this.members.filter( + m => m.user._id !== this.owner._id + ) + return (this.result = this.handler.buildOwnerAndMembersViews( + this.membersWithoutOwner + )) + }) + + it('should produce an object with the right keys', function() { + return expect(this.result).to.have.all.keys([ + 'owner', + 'ownerFeatures', + 'members' + ]) + }) + + it('should not separate out an owner', function() { + this.result.members.length.should.equal(this.membersWithoutOwner.length) + return expect(this.result.owner).to.equal(null) + }) + + return it('should not extract the ownerFeatures from the owner object', function() { + return expect(this.result.ownerFeatures).to.equal(null) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectEntityHandlerTests.js b/services/web/test/unit/src/Project/ProjectEntityHandlerTests.js new file mode 100644 index 0000000000..59091b5aa6 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectEntityHandlerTests.js @@ -0,0 +1,384 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const { assert } = require('chai') +const should = chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Project/ProjectEntityHandler' +const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongoose').Types +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('ProjectEntityHandler', function() { + const project_id = '4eecb1c1bffa66588e0000a1' + const doc_id = '4eecb1c1bffa66588e0000a2' + const folder_id = '4eecaffcbffa66588e000008' + const rootFolderId = '4eecaffcbffa66588e000007' + const userId = 1234 + + beforeEach(function() { + let Project + this.TpdsUpdateSender = { + addDoc: sinon.stub().callsArg(1), + addFile: sinon.stub().callsArg(1) + } + this.ProjectModel = Project = (function() { + Project = class Project { + static initClass() { + this.prototype.rootFolder = [this.rootFolder] + } + constructor(options) { + this._id = project_id + this.name = 'project_name_here' + this.rev = 0 + } + } + Project.initClass() + return Project + })() + + this.project = new this.ProjectModel() + + this.ProjectLocator = { findElement: sinon.stub() } + this.DocumentUpdaterHandler = { + updateProjectStructure: sinon.stub().yields() + } + + this.callback = sinon.stub() + + return (this.ProjectEntityHandler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }), + '../Docstore/DocstoreManager': (this.DocstoreManager = {}), + '../../Features/DocumentUpdater/DocumentUpdaterHandler': this + .DocumentUpdaterHandler, + '../../models/Project': { + Project: this.ProjectModel + }, + './ProjectLocator': this.ProjectLocator, + './ProjectGetter': (this.ProjectGetter = {}), + '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender + } + })) + }) + + describe('getting folders, docs and files', function() { + beforeEach(function() { + this.project.rootFolder = [ + { + docs: [ + (this.doc1 = { + name: 'doc1', + _id: 'doc1_id' + }) + ], + fileRefs: [ + (this.file1 = { + rev: 1, + _id: 'file1_id', + name: 'file1' + }) + ], + folders: [ + (this.folder1 = { + name: 'folder1', + docs: [ + (this.doc2 = { + name: 'doc2', + _id: 'doc2_id' + }) + ], + fileRefs: [ + (this.file2 = { + rev: 2, + name: 'file2', + _id: 'file2_id' + }) + ], + folders: [] + }) + ] + } + ] + return (this.ProjectGetter.getProjectWithoutDocLines = sinon + .stub() + .yields(null, this.project)) + }) + + describe('getAllDocs', function() { + beforeEach(function() { + this.docs = [ + { + _id: this.doc1._id, + lines: (this.lines1 = ['one']), + rev: (this.rev1 = 1) + }, + { + _id: this.doc2._id, + lines: (this.lines2 = ['two']), + rev: (this.rev2 = 2) + } + ] + this.DocstoreManager.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + return this.ProjectEntityHandler.getAllDocs(project_id, this.callback) + }) + + it('should get the doc lines and rev from the docstore', function() { + return this.DocstoreManager.getAllDocs + .calledWith(project_id) + .should.equal(true) + }) + + return it('should call the callback with the docs with the lines and rev included', function() { + return this.callback + .calledWith(null, { + '/doc1': { + _id: this.doc1._id, + lines: this.lines1, + name: this.doc1.name, + rev: this.rev1 + }, + '/folder1/doc2': { + _id: this.doc2._id, + lines: this.lines2, + name: this.doc2.name, + rev: this.rev2 + } + }) + .should.equal(true) + }) + }) + + describe('getAllFiles', function() { + beforeEach(function() { + this.callback = sinon.stub() + return this.ProjectEntityHandler.getAllFiles(project_id, this.callback) + }) + + return it('should call the callback with the files', function() { + return this.callback + .calledWith(null, { + '/file1': this.file1, + '/folder1/file2': this.file2 + }) + .should.equal(true) + }) + }) + + describe('getAllDocPathsFromProject', function() { + beforeEach(function() { + this.docs = [ + { + _id: this.doc1._id, + lines: (this.lines1 = ['one']), + rev: (this.rev1 = 1) + }, + { + _id: this.doc2._id, + lines: (this.lines2 = ['two']), + rev: (this.rev2 = 2) + } + ] + this.callback = sinon.stub() + return this.ProjectEntityHandler.getAllDocPathsFromProject( + this.project, + this.callback + ) + }) + + return it('should call the callback with the path for each doc_id', function() { + this.expected = {} + this.expected[this.doc1._id] = `/${this.doc1.name}` + this.expected[this.doc2._id] = `/folder1/${this.doc2.name}` + return this.callback.calledWith(null, this.expected).should.equal(true) + }) + }) + + describe('_getAllFolders', function() { + beforeEach(function() { + this.callback = sinon.stub() + return this.ProjectEntityHandler._getAllFolders( + project_id, + this.callback + ) + }) + + it('should get the project without the docs lines', function() { + return this.ProjectGetter.getProjectWithoutDocLines + .calledWith(project_id) + .should.equal(true) + }) + + return it('should call the callback with the folders', function() { + return this.callback + .calledWith(null, { + '/': this.project.rootFolder[0], + '/folder1': this.folder1 + }) + .should.equal(true) + }) + }) + + return describe('_getAllFoldersFromProject', function() { + beforeEach(function() { + this.callback = sinon.stub() + return this.ProjectEntityHandler._getAllFoldersFromProject( + this.project, + this.callback + ) + }) + + return it('should call the callback with the folders', function() { + return this.callback + .calledWith(null, { + '/': this.project.rootFolder[0], + '/folder1': this.folder1 + }) + .should.equal(true) + }) + }) + }) + + describe('flushProjectToThirdPartyDataStore', function() { + beforeEach(function(done) { + this.project = { + _id: project_id, + name: 'Mock project name' + } + this.DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().yields() + this.docs = { + '/doc/one': (this.doc1 = { _id: 'mock-doc-1', lines: ['one'], rev: 5 }), + '/doc/two': (this.doc2 = { _id: 'mock-doc-2', lines: ['two'], rev: 6 }) + } + this.files = { + '/file/one': (this.file1 = { _id: 'mock-file-1', rev: 7 }), + '/file/two': (this.file2 = { _id: 'mock-file-2', rev: 8 }) + } + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .yields(null, this.docs) + this.ProjectEntityHandler.getAllFiles = sinon + .stub() + .yields(null, this.files) + + this.ProjectGetter.getProject = sinon.stub().yields(null, this.project) + + return this.ProjectEntityHandler.flushProjectToThirdPartyDataStore( + project_id, + () => done() + ) + }) + + it('should flush the project from the doc updater', function() { + return this.DocumentUpdaterHandler.flushProjectToMongo + .calledWith(project_id) + .should.equal(true) + }) + + it('should look up the project in mongo', function() { + return this.ProjectGetter.getProject + .calledWith(project_id) + .should.equal(true) + }) + + it('should get all the docs in the project', function() { + return this.ProjectEntityHandler.getAllDocs + .calledWith(project_id) + .should.equal(true) + }) + + it('should get all the files in the project', function() { + return this.ProjectEntityHandler.getAllFiles + .calledWith(project_id) + .should.equal(true) + }) + + it('should flush each doc to the TPDS', function() { + return (() => { + const result = [] + for (let path in this.docs) { + const doc = this.docs[path] + result.push( + this.TpdsUpdateSender.addDoc + .calledWith({ + project_id, + doc_id: doc._id, + project_name: this.project.name, + rev: doc.rev, + path + }) + .should.equal(true) + ) + } + return result + })() + }) + + return it('should flush each file to the TPDS', function() { + return (() => { + const result = [] + for (let path in this.files) { + const file = this.files[path] + result.push( + this.TpdsUpdateSender.addFile + .calledWith({ + project_id, + file_id: file._id, + project_name: this.project.name, + rev: file.rev, + path + }) + .should.equal(true) + ) + } + return result + })() + }) + }) + + return describe('getDoc', function() { + beforeEach(function() { + this.lines = ['mock', 'doc', 'lines'] + this.rev = 5 + this.version = 42 + this.ranges = { mock: 'ranges' } + + this.DocstoreManager.getDoc = sinon + .stub() + .callsArgWith(3, null, this.lines, this.rev, this.version, this.ranges) + return this.ProjectEntityHandler.getDoc(project_id, doc_id, this.callback) + }) + + it('should call the docstore', function() { + return this.DocstoreManager.getDoc + .calledWith(project_id, doc_id) + .should.equal(true) + }) + + return it('should call the callback with the lines, version and rev', function() { + return this.callback + .calledWith(null, this.lines, this.rev, this.version, this.ranges) + .should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandlerTests.js b/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandlerTests.js new file mode 100644 index 0000000000..94831db594 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectEntityMongoUpdateHandlerTests.js @@ -0,0 +1,1265 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const { expect } = chai +const { assert } = require('chai') +const should = chai.should() +const sinon = require('sinon') +const tk = require('timekeeper') +const modulePath = + '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler' +const Errors = require('../../../../app/src/Features/Errors/Errors') +const { ObjectId } = require('mongoose').Types +const SandboxedModule = require('sandboxed-module') + +describe('ProjectEntityMongoUpdateHandler', function() { + const project_id = '4eecb1c1bffa66588e0000a1' + const doc_id = '4eecb1c1bffa66588e0000a2' + const file_id = '4eecb1c1bffa66588e0000a3' + const folder_id = '4eecaffcbffa66588e000008' + + beforeEach(function() { + let Folder + this.FolderModel = Folder = class Folder { + constructor(options) { + ;({ name: this.name } = options) + this._id = 'folder_id' + } + } + + this.docName = 'doc-name' + this.fileName = 'something.jpg' + this.project = { _id: project_id, name: 'project name' } + + this.callback = sinon.stub() + + tk.freeze(Date.now()) + return (this.subject = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }), + 'settings-sharelatex': (this.settings = { + maxEntitiesPerProject: 100 + }), + '../Cooldown/CooldownManager': (this.CooldownManager = {}), + '../../models/Folder': { + Folder: this.FolderModel + }, + '../../infrastructure/LockManager': (this.LockManager = { + runWithLock: sinon.spy((namespace, id, runner, callback) => + runner(callback) + ) + }), + '../../models/Project': { + Project: (this.ProjectModel = {}) + }, + './ProjectEntityHandler': (this.ProjectEntityHandler = {}), + './ProjectLocator': (this.ProjectLocator = {}), + './ProjectGetter': (this.ProjectGetter = { + getProjectWithoutLock: sinon.stub().yields(null, this.project) + }) + } + })) + }) + + afterEach(() => tk.reset()) + + describe('addDoc', function() { + beforeEach(function() { + this.subject._confirmFolder = sinon.stub().yields(folder_id) + this.subject._putElement = sinon.stub() + + this.doc = { _id: doc_id } + return this.subject.addDoc(project_id, folder_id, this.doc, this.callback) + }) + + it('gets the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('checks the folder exists', function() { + return this.subject._confirmFolder + .calledWith(this.project, folder_id) + .should.equal(true) + }) + + return it('puts the element in mongo', function() { + return this.subject._putElement + .calledWith(this.project, folder_id, this.doc, 'doc', this.callback) + .should.equal(true) + }) + }) + + describe('addFile', function() { + beforeEach(function() { + this.subject._confirmFolder = sinon.stub().yields(folder_id) + this.subject._putElement = sinon.stub() + + this.file = { _id: file_id } + return this.subject.addFile( + project_id, + folder_id, + this.file, + this.callback + ) + }) + + it('gets the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('checks the folder exists', function() { + return this.subject._confirmFolder + .calledWith(this.project, folder_id) + .should.equal(true) + }) + + return it('puts the element in mongo', function() { + return this.subject._putElement + .calledWith(this.project, folder_id, this.file, 'file', this.callback) + .should.equal(true) + }) + }) + + describe('replaceFileWithNew', function() { + beforeEach(function() { + this.file = { _id: file_id } + this.path = { mongo: 'file.png' } + this.newFile = { _id: 'new-file-id' } + this.newFile.linkedFileData = this.linkedFileData = { provider: 'url' } + this.newProject = 'new-project' + this.ProjectLocator.findElement = sinon + .stub() + .yields(null, this.file, this.path) + this.ProjectModel.findOneAndUpdate = sinon + .stub() + .yields(null, this.newProject) + this.ProjectModel.update = sinon.stub().yields() + + return this.subject.replaceFileWithNew( + project_id, + file_id, + this.newFile, + this.callback + ) + }) + + it('gets the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('finds the existing element', function() { + return this.ProjectLocator.findElement + .calledWith({ + project: this.project, + element_id: file_id, + type: 'file' + }) + .should.equal(true) + }) + + it('inserts a deletedFile reference for the old file', function() { + return this.ProjectModel.update + .calledWith( + { _id: project_id }, + { + $push: { + deletedFiles: { + _id: file_id, + name: this.file.name, + linkedFileData: this.file.linkedFileData, + hash: this.file.hash, + deletedAt: new Date() + } + } + } + ) + .should.equal(true) + }) + + it('increments the project version and sets the rev and created_at', function() { + return this.ProjectModel.findOneAndUpdate + .calledWith( + { _id: project_id }, + { + $inc: { version: 1, 'file.png.rev': 1 }, + $set: { + 'file.png._id': this.newFile._id, + 'file.png.created': new Date(), + 'file.png.linkedFileData': this.linkedFileData, + 'file.png.hash': this.hash + } + }, + { new: true } + ) + .should.equal(true) + }) + + return it('calls the callback', function() { + return this.callback + .calledWith(null, this.file, this.project, this.path, this.newProject) + .should.equal(true) + }) + }) + + describe('mkdirp', function() { + beforeEach(function() { + this.parentFolder_id = '1jnjknjk' + this.newFolder = { _id: 'newFolder_id_here' } + this.lastFolder = { _id: '123das', folders: [] } + + this.rootFolder = { _id: 'rootFolderId' } + this.project = { _id: project_id, rootFolder: [this.rootFolder] } + + this.ProjectGetter.getProjectWithOnlyFolders = sinon + .stub() + .yields(null, this.project) + this.ProjectLocator.findElementByPath = function() {} + sinon.stub(this.ProjectLocator, 'findElementByPath', (options, cb) => { + const { path } = options + this.parentFolder = { _id: 'parentFolder_id_here' } + const lastFolder = path.substring(path.lastIndexOf('/')) + if (lastFolder.indexOf('level1') === -1) { + return cb('level1 is not the last foler ') + } else { + return cb(null, this.parentFolder) + } + }) + return (this.subject.addFolder = { + withoutLock: (project_id, parentFolder_id, folderName, callback) => { + return callback(null, { name: folderName }, this.parentFolder_id) + } + }) + }) + + it('should return the root folder if the path is just a slash', function(done) { + const path = '/' + return this.subject.mkdirp( + project_id, + path, + {}, + (err, folders, lastFolder) => { + lastFolder.should.deep.equal(this.rootFolder) + assert.equal(lastFolder.parentFolder_id, undefined) + return done() + } + ) + }) + + it('should make just one folder', function(done) { + const path = '/differentFolder/' + return this.subject.mkdirp( + project_id, + path, + {}, + (err, folders, lastFolder) => { + folders.length.should.equal(1) + lastFolder.name.should.equal('differentFolder') + lastFolder.parentFolder_id.should.equal(this.parentFolder_id) + return done() + } + ) + }) + + it('should make the final folder in path if it doesnt exist with one level', function(done) { + const path = 'level1/level2' + return this.subject.mkdirp( + project_id, + path, + {}, + (err, folders, lastFolder) => { + folders.length.should.equal(1) + lastFolder.name.should.equal('level2') + lastFolder.parentFolder_id.should.equal(this.parentFolder_id) + return done() + } + ) + }) + + it('should make the final folder in path if it doesnt exist with mutliple levels', function(done) { + const path = 'level1/level2/level3' + + return this.subject.mkdirp( + project_id, + path, + {}, + (err, folders, lastFolder) => { + folders.length.should.equal(2) + folders[0].name.should.equal('level2') + folders[0].parentFolder_id.should.equal(this.parentFolder_id) + lastFolder.name.should.equal('level3') + lastFolder.parentFolder_id.should.equal(this.parentFolder_id) + return done() + } + ) + }) + + it('should work with slashes either side', function(done) { + const path = '/level1/level2/level3/' + + return this.subject.mkdirp( + project_id, + path, + {}, + (err, folders, lastFolder) => { + folders.length.should.equal(2) + folders[0].name.should.equal('level2') + folders[0].parentFolder_id.should.equal(this.parentFolder_id) + lastFolder.name.should.equal('level3') + lastFolder.parentFolder_id.should.equal(this.parentFolder_id) + return done() + } + ) + }) + + it('should use a case-insensitive match by default', function(done) { + const path = '/differentFolder/' + return this.subject.mkdirp( + project_id, + path, + {}, + (err, folders, lastFolder) => { + this.ProjectLocator.findElementByPath + .calledWithMatch({ exactCaseMatch: undefined }) + .should.equal(true) + return done() + } + ) + }) + + return it('should use a case-sensitive match if exactCaseMatch option is set', function(done) { + const path = '/differentFolder/' + return this.subject.mkdirp( + project_id, + path, + { exactCaseMatch: true }, + (err, folders, lastFolder) => { + this.ProjectLocator.findElementByPath + .calledWithMatch({ exactCaseMatch: true }) + .should.equal(true) + return done() + } + ) + }) + }) + + describe('moveEntity', function() { + beforeEach(function() { + this.pathAfterMove = { + fileSystem: '/somewhere/else.txt' + } + + this.ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() + this.ProjectEntityHandler.getAllEntitiesFromProject + .onFirstCall() + .yields( + null, + (this.oldDocs = ['old-doc']), + (this.oldFiles = ['old-file']) + ) + this.ProjectEntityHandler.getAllEntitiesFromProject + .onSecondCall() + .yields( + null, + (this.newDocs = ['new-doc']), + (this.newFiles = ['new-file']) + ) + + this.doc = { lines: ['1234', '312343d'], rev: '1234' } + this.path = { + mongo: 'folders[0]', + fileSystem: '/old_folder/somewhere.txt' + } + this.newProject = 'new-project' + this.ProjectLocator.findElement = sinon + .stub() + .withArgs({ + project: this.project, + element_id: this.docId, + type: 'docs' + }) + .yields(null, this.doc, this.path) + + this.subject._checkValidMove = sinon.stub().yields() + + this.subject._removeElementFromMongoArray = sinon + .stub() + .yields(null, this.newProject) + this.subject._putElement = sinon + .stub() + .yields(null, { path: this.pathAfterMove }, this.newProject) + + return this.subject.moveEntity( + project_id, + doc_id, + folder_id, + 'docs', + this.callback + ) + }) + + it('should get the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('should find the doc to move', function() { + return this.ProjectLocator.findElement + .calledWith({ element_id: doc_id, type: 'docs', project: this.project }) + .should.equal(true) + }) + + it('should check this is a valid move', function() { + return this.subject._checkValidMove + .calledWith(this.project, 'docs', this.doc, this.path, folder_id) + .should.equal(true) + }) + + it('should put the element in the new folder', function() { + return this.subject._putElement + .calledWith(this.project, folder_id, this.doc, 'docs') + .should.equal(true) + }) + + it('should remove the element from its current position', function() { + return this.subject._removeElementFromMongoArray + .calledWith(this.ProjectModel, project_id, this.path.mongo, doc_id) + .should.equal(true) + }) + + it('should remove the element from its current position after putting the element in the new folder', function() { + return this.subject._removeElementFromMongoArray + .calledAfter(this.subject._putElement) + .should.equal(true) + }) + + return it('calls the callback', function() { + const changes = { + oldDocs: this.oldDocs, + newDocs: this.newDocs, + oldFiles: this.oldFiles, + newFiles: this.newFiles, + newProject: this.newProject + } + return this.callback + .calledWith( + null, + this.project, + this.path.fileSystem, + this.pathAfterMove.fileSystem, + this.doc.rev, + changes + ) + .should.equal(true) + }) + }) + + describe('moveEntity must refuse to move the folder to a subfolder of itself', function() { + beforeEach(function() { + this.pathAfterMove = { + fileSystem: '/somewhere/else.txt' + } + + this.doc = { lines: ['1234', '312343d'], rev: '1234' } + this.path = { + mongo: 'folders[0]', + fileSystem: '/old_folder/somewhere.txt' + } + this.newProject = 'new-project' + this.ProjectLocator.findElement = sinon + .stub() + .withArgs({ + project: this.project, + element_id: this.docId, + type: 'docs' + }) + .yields(null, this.doc, this.path) + + // return an error when moving a folder to a subfolder of itself + this.subject._checkValidMove = sinon.stub().yields(new Error()) + + this.subject._removeElementFromMongoArray = sinon + .stub() + .yields(null, this.project) + this.subject._putElement = sinon + .stub() + .yields(null, { path: this.pathAfterMove }, this.newProject) + + return this.subject.moveEntity( + project_id, + doc_id, + folder_id, + 'docs', + this.callback + ) + }) + + it('should get the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('should find the doc to move', function() { + return this.ProjectLocator.findElement + .calledWith({ element_id: doc_id, type: 'docs', project: this.project }) + .should.equal(true) + }) + + it('should check this is an invalid move', function() { + return this.subject._checkValidMove + .calledWith(this.project, 'docs', this.doc, this.path, folder_id) + .should.equal(true) + }) + + it('should not put the element in the new folder', function() { + return this.subject._putElement.called.should.equal(false) + }) + + it('should not remove the element from its current position', function() { + return this.subject._removeElementFromMongoArray.called.should.equal( + false + ) + }) + + return it('calls the callback with an error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + + describe('deleteEntity', function() { + beforeEach(function() { + this.path = { mongo: 'mongo.path', fileSystem: '/file/system/path' } + this.doc = { _id: doc_id } + this.ProjectLocator.findElement = sinon + .stub() + .callsArgWith(1, null, this.doc, this.path) + this.subject._removeElementFromMongoArray = sinon.stub().yields() + return this.subject.deleteEntity(project_id, doc_id, 'doc', this.callback) + }) + + it('should get the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('should find the element', function() { + return this.ProjectLocator.findElement + .calledWith({ + project: this.project, + element_id: this.doc._id, + type: 'doc' + }) + .should.equal(true) + }) + + it('should remove the element from the database', function() { + return this.subject._removeElementFromMongoArray + .calledWith( + this.ProjectModel, + project_id, + this.path.mongo, + this.doc._id + ) + .should.equal(true) + }) + + return it('calls the callbck', function() { + return this.callback + .calledWith(null, this.doc, this.path, this.project) + .should.equal(true) + }) + }) + + describe('renameEntity', function() { + beforeEach(function() { + this.newName = 'new.tex' + this.path = { mongo: 'mongo.path', fileSystem: '/old.tex' } + + this.project = { + _id: ObjectId(project_id), + rootFolder: [{ _id: ObjectId() }] + } + this.doc = { _id: doc_id, name: 'old.tex', rev: 1 } + this.folder = { _id: folder_id } + this.newProject = 'new-project' + + this.ProjectGetter.getProjectWithoutLock = sinon + .stub() + .yields(null, this.project) + + this.ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() + this.ProjectEntityHandler.getAllEntitiesFromProject + .onFirstCall() + .yields( + null, + (this.oldDocs = ['old-doc']), + (this.oldFiles = ['old-file']) + ) + this.ProjectEntityHandler.getAllEntitiesFromProject + .onSecondCall() + .yields( + null, + (this.newDocs = ['new-doc']), + (this.newFiles = ['new-file']) + ) + + this.ProjectLocator.findElement = sinon + .stub() + .yields(null, this.doc, this.path, this.folder) + this.subject._checkValidElementName = sinon.stub().yields() + this.ProjectModel.findOneAndUpdate = sinon + .stub() + .callsArgWith(3, null, this.newProject) + + return this.subject.renameEntity( + project_id, + doc_id, + 'doc', + this.newName, + this.callback + ) + }) + + it('should get the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('should find the doc', function() { + return this.ProjectLocator.findElement + .calledWith({ element_id: doc_id, type: 'doc', project: this.project }) + .should.equal(true) + }) + + it('should check the new name is valid', function() { + return this.subject._checkValidElementName + .calledWith(this.folder, this.newName) + .should.equal(true) + }) + + it('should update the doc name', function() { + return this.ProjectModel.findOneAndUpdate + .calledWith( + { _id: project_id }, + { $set: { 'mongo.path.name': this.newName }, $inc: { version: 1 } }, + { new: true } + ) + .should.equal(true) + }) + + return it('calls the callback', function() { + const changes = { + oldDocs: this.oldDocs, + newDocs: this.newDocs, + oldFiles: this.oldFiles, + newFiles: this.newFiles, + newProject: this.newProject + } + return this.callback + .calledWith( + null, + this.project, + '/old.tex', + '/new.tex', + this.doc.rev, + changes + ) + .should.equal(true) + }) + }) + + describe('addFolder', function() { + beforeEach(function() { + this.folderName = 'folder1234' + this.ProjectGetter.getProjectWithOnlyFolders = sinon + .stub() + .callsArgWith(1, null, this.project) + this.subject._confirmFolder = sinon.stub().yields(folder_id) + this.subject._putElement = sinon.stub().yields() + + return this.subject.addFolder( + project_id, + folder_id, + this.folderName, + this.callback + ) + }) + + it('gets the project', function() { + return this.ProjectGetter.getProjectWithoutLock + .calledWith(project_id, { + rootFolder: true, + name: true, + overleaf: true + }) + .should.equal(true) + }) + + it('checks the parent folder exists', function() { + return this.subject._confirmFolder + .calledWith(this.project, folder_id) + .should.equal(true) + }) + + it('puts the element in mongo', function() { + const folderMatcher = sinon.match(folder => { + return folder.name === this.folderName + }) + + return this.subject._putElement + .calledWithMatch(this.project, folder_id, folderMatcher, 'folder') + .should.equal(true) + }) + + return it('calls the callback', function() { + const folderMatcher = sinon.match(folder => { + return folder.name === this.folderName + }) + + return this.callback + .calledWithMatch(null, folderMatcher, folder_id) + .should.equal(true) + }) + }) + + describe('_removeElementFromMongoArray ', function() { + beforeEach(function() { + this.mongoPath = 'folders[0].folders[5]' + this.id = '12344' + this.entityId = '5678' + this.ProjectModel.update = sinon.stub().yields() + this.ProjectModel.findOneAndUpdate = sinon + .stub() + .yields(null, this.project) + return this.subject._removeElementFromMongoArray( + this.ProjectModel, + this.id, + this.mongoPath, + this.entityId, + this.callback + ) + }) + + it('should pull', function() { + return this.ProjectModel.findOneAndUpdate + .calledWith( + { _id: this.id }, + { + $pull: { 'folders[0]': { _id: this.entityId } }, + $inc: { version: 1 } + }, + { new: true } + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.calledWith(null, this.project).should.equal(true) + }) + }) + + describe('_countElements', function() { + beforeEach(function() { + return (this.project = { + _id: project_id, + rootFolder: [ + { + docs: [{ _id: 123 }, { _id: 345 }], + fileRefs: [{ _id: 123 }, { _id: 345 }, { _id: 456 }], + folders: [ + { + docs: [{ _id: 123 }, { _id: 345 }, { _id: 456 }], + fileRefs: {}, + folders: [ + { + docs: [{ _id: 1234 }], + fileRefs: [{ _id: 23123 }, { _id: 123213 }, { _id: 2312 }], + folders: [ + { + docs: [{ _id: 321321 }, { _id: 123213 }], + fileRefs: [{ _id: 312321 }], + folders: [] + } + ] + } + ] + }, + { + docs: [{ _id: 123 }, { _id: 32131 }], + fileRefs: [], + folders: [ + { + docs: [{ _id: 3123 }], + fileRefs: [ + { _id: 321321 }, + { _id: 321321 }, + { _id: 313122 } + ], + folders: 0 + } + ] + } + ] + } + ] + }) + }) + + it('should return the correct number', function() { + return expect(this.subject._countElements(this.project)).to.equal(26) + }) + + it('should deal with null folders', function() { + this.project.rootFolder[0].folders[0].folders = undefined + return expect(this.subject._countElements(this.project)).to.equal(17) + }) + + it('should deal with null docs', function() { + this.project.rootFolder[0].folders[0].docs = undefined + return expect(this.subject._countElements(this.project)).to.equal(23) + }) + + return it('should deal with null fileRefs', function() { + this.project.rootFolder[0].folders[0].folders[0].fileRefs = undefined + return expect(this.subject._countElements(this.project)).to.equal(23) + }) + }) + + describe('_putElement', function() { + beforeEach(function() { + this.project = { + _id: project_id, + rootFolder: [{ _id: ObjectId() }] + } + this.folder = { + _id: ObjectId(), + name: 'someFolder', + docs: [{ name: 'another-doc.tex' }], + fileRefs: [{ name: 'another-file.tex' }], + folders: [{ name: 'another-folder' }] + } + this.doc = { + _id: ObjectId(), + name: 'new.tex' + } + this.path = { mongo: 'mongo.path', fileSystem: '/file/system/old.tex' } + this.ProjectLocator.findElement = sinon + .stub() + .yields(null, this.folder, this.path) + return (this.ProjectModel.findOneAndUpdate = sinon + .stub() + .yields(null, this.project)) + }) + + return describe('updating the project', function() { + it('should use the correct mongo path', function(done) { + return this.subject._putElement( + this.project, + this.folder._id, + this.doc, + 'docs', + err => { + this.ProjectModel.findOneAndUpdate.args[0][0]._id.should.equal( + this.project._id + ) + assert.deepEqual( + this.ProjectModel.findOneAndUpdate.args[0][1].$push[ + this.path.mongo + '.docs' + ], + this.doc + ) + return done() + } + ) + }) + + it('should return the project in the callback', function(done) { + return this.subject._putElement( + this.project, + this.folder._id, + this.doc, + 'docs', + (err, path, project) => { + assert.equal(project, this.project) + return done() + } + ) + }) + + it('should add an s onto the type if not included', function(done) { + return this.subject._putElement( + this.project, + this.folder._id, + this.doc, + 'doc', + err => { + assert.deepEqual( + this.ProjectModel.findOneAndUpdate.args[0][1].$push[ + this.path.mongo + '.docs' + ], + this.doc + ) + return done() + } + ) + }) + + it('should not call update if element is null', function(done) { + return this.subject._putElement( + this.project, + this.folder._id, + null, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + return done() + } + ) + }) + + it('should default to root folder insert', function(done) { + return this.subject._putElement( + this.project, + null, + this.doc, + 'doc', + err => { + this.ProjectLocator.findElement.args[0][0].element_id.should.equal( + this.project.rootFolder[0]._id + ) + return done() + } + ) + }) + + it('should error if the element has no _id', function(done) { + const doc = { name: 'something' } + return this.subject._putElement( + this.project, + this.folder._id, + doc, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + return done() + } + ) + }) + + it('should error if element name contains invalid characters', function(done) { + const doc = { + _id: ObjectId(), + name: 'something*bad' + } + return this.subject._putElement( + this.project, + this.folder._id, + doc, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + err.should.deep.equal( + new Errors.InvalidNameError('invalid element name') + ) + return done() + } + ) + }) + + it('should error if element name is too long', function(done) { + const doc = { + _id: ObjectId(), + name: new Array(200).join('long-') + 'something' + } + return this.subject._putElement( + this.project, + this.folder._id, + doc, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + err.should.deep.equal( + new Errors.InvalidNameError('invalid element name') + ) + return done() + } + ) + }) + + it('should error if the folder name is too long', function(done) { + this.path = { + mongo: 'mongo.path', + fileSystem: new Array(200).join('subdir/') + 'foo' + } + this.ProjectLocator.findElement.callsArgWith( + 1, + null, + this.folder, + this.path + ) + const doc = { + _id: ObjectId(), + name: 'something' + } + return this.subject._putElement( + this.project, + this.folder._id, + doc, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + err.should.deep.equal(new Errors.InvalidNameError('path too long')) + return done() + } + ) + }) + + it('should error if a document already exists with the same name', function(done) { + const doc = { + _id: ObjectId(), + name: 'another-doc.tex' + } + return this.subject._putElement( + this.project, + this.folder, + doc, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + err.should.deep.equal( + new Errors.InvalidNameError('file already exists') + ) + return done() + } + ) + }) + + it('should error if a file already exists with the same name', function(done) { + const doc = { + _id: ObjectId(), + name: 'another-file.tex' + } + return this.subject._putElement( + this.project, + this.folder, + doc, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + err.should.deep.equal( + new Errors.InvalidNameError('file already exists') + ) + return done() + } + ) + }) + + return it('should error if a folder already exists with the same name', function(done) { + const doc = { + _id: ObjectId(), + name: 'another-folder' + } + return this.subject._putElement( + this.project, + this.folder, + doc, + 'doc', + err => { + this.ProjectModel.findOneAndUpdate.called.should.equal(false) + err.should.deep.equal( + new Errors.InvalidNameError('file already exists') + ) + return done() + } + ) + }) + }) + }) + + describe('_checkValidElementName', function() { + beforeEach(function() { + return (this.folder = { + docs: [{ name: 'doc_name' }], + fileRefs: [{ name: 'file_name' }], + folders: [{ name: 'folder_name' }] + }) + }) + + it('returns an error if name matches any doc name', function() { + return this.subject._checkValidElementName(this.folder, 'doc_name', err => + expect(err).to.deep.equal( + new Errors.InvalidNameError('file already exists') + ) + ) + }) + + it('returns an error if name matches any file name', function() { + return this.subject._checkValidElementName( + this.folder, + 'file_name', + err => + expect(err).to.deep.equal( + new Errors.InvalidNameError('file already exists') + ) + ) + }) + + it('returns an error if name matches any folder name', function() { + return this.subject._checkValidElementName( + this.folder, + 'folder_name', + err => + expect(err).to.deep.equal( + new Errors.InvalidNameError('file already exists') + ) + ) + }) + + return it('returns nothing if name is valid', function() { + return this.subject._checkValidElementName( + this.folder, + 'unique_name', + err => expect(err).to.be.undefined + ) + }) + }) + + describe('_checkValidMove', function() { + beforeEach(function() { + this.destFolder = { _id: folder_id } + this.destFolderPath = { fileSystem: '/foo/bar' } + this.ProjectLocator.findElement = sinon + .stub() + .yields(null, this.destFolder, this.destFolderPath) + return (this.subject._checkValidElementName = sinon.stub().yields()) + }) + + it('checks the element name is valid', function() { + this.doc = { _id: doc_id, name: 'doc_name' } + return this.subject._checkValidMove( + this.project, + 'doc', + this.doc, + { fileSystem: '/main.tex' }, + this.destFolder._id, + err => { + expect(err).to.be.undefined + return this.subject._checkValidElementName + .calledWith(this.destFolder, this.doc.name) + .should.equal(true) + } + ) + }) + + return it('returns an error if trying to move a folder inside itself', function() { + const folder = { name: 'folder_name' } + return this.subject._checkValidMove( + this.project, + 'folder', + folder, + { fileSystem: '/foo' }, + this.destFolder._id, + err => { + return expect(err).to.deep.equal( + new Errors.InvalidNameError( + 'destination folder is a child folder of me' + ) + ) + } + ) + }) + }) + + return describe('_insertDeletedDocReference', function() { + beforeEach(function() { + this.doc = { + _id: ObjectId(), + name: 'test.tex' + } + this.callback = sinon.stub() + this.ProjectModel.update = sinon.stub().yields() + return this.subject._insertDeletedDocReference( + project_id, + this.doc, + this.callback + ) + }) + + it('should insert the doc into deletedDocs', function() { + return this.ProjectModel.update + .calledWith( + { + _id: project_id + }, + { + $push: { + deletedDocs: { + _id: this.doc._id, + name: this.doc.name, + deletedAt: new Date() + } + } + } + ) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js b/services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js new file mode 100644 index 0000000000..db789b809f --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js @@ -0,0 +1,2013 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const { assert } = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Project/ProjectEntityUpdateHandler' +const sinon = require('sinon') +const Errors = require('../../../../app/src/Features/Errors/Errors') +const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongoose').Types + +describe('ProjectEntityUpdateHandler', function() { + const project_id = '4eecb1c1bffa66588e0000a1' + const projectHistoryId = '123456' + const doc_id = '4eecb1c1bffa66588e0000a2' + const file_id = '4eecaffcbffa66588e000009' + const folder_id = '4eecaffcbffa66588e000008' + const rootFolderId = '4eecaffcbffa66588e000007' + const new_file_id = '4eecaffcbffa66588e000099' + const userId = 1234 + + beforeEach(function() { + let Doc, File + this.project = { + _id: project_id, + name: 'project name', + overleaf: { + history: { + id: projectHistoryId + } + } + } + this.fileUrl = 'filestore.example.com/file' + this.FileStoreHandler = {} + + this.DocModel = Doc = class Doc { + constructor(options) { + ;({ name: this.name, lines: this.lines } = options) + this._id = doc_id + this.rev = 0 + } + } + this.FileModel = File = class File { + constructor(options) { + ;({ name: this.name } = options) + // use a new id for replacement files + if (this.name === 'dummy-upload-filename') { + this._id = new_file_id + } else { + this._id = file_id + } + this.rev = 0 + if (options.linkedFileData != null) { + this.linkedFileData = options.linkedFileData + } + if (options.hash != null) { + this.hash = options.hash + } + } + } + this.docName = 'doc-name' + this.docLines = ['1234', 'abc'] + + this.fileName = 'something.jpg' + this.fileSystemPath = 'somehintg' + + this.linkedFileData = { provider: 'url' } + + this.source = 'editor' + this.callback = sinon.stub() + return (this.ProjectEntityUpdateHandler = SandboxedModule.require( + modulePath, + { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }), + '../../models/Doc': { + Doc: this.DocModel + }, + '../Docstore/DocstoreManager': (this.DocstoreManager = {}), + '../Errors/Errors': Errors, + '../../Features/DocumentUpdater/DocumentUpdaterHandler': (this.DocumentUpdaterHandler = { + updateProjectStructure: sinon.stub().yields() + }), + '../../models/File': { + File: this.FileModel + }, + '../FileStore/FileStoreHandler': this.FileStoreHandler, + '../../infrastructure/LockManager': (this.LockManager = { + runWithLock: sinon.spy((namespace, id, runner, callback) => + runner(callback) + ) + }), + '../../models/Project': { + Project: (this.ProjectModel = {}) + }, + './ProjectGetter': (this.ProjectGetter = {}), + './ProjectLocator': (this.ProjectLocator = {}), + './ProjectUpdateHandler': (this.ProjectUpdater = {}), + './ProjectEntityHandler': (this.ProjectEntityHandler = {}), + './ProjectEntityMongoUpdateHandler': (this.ProjectEntityMongoUpdateHandler = {}), + '../ThirdPartyDataStore/TpdsUpdateSender': (this.TpdsUpdateSender = { + addFile: sinon.stub().yields() + }) + } + } + )) + }) + + describe('copyFileFromExistingProjectWithProject', function() { + beforeEach(function() { + this.oldProject_id = '123kljadas' + this.oldFileRef = { name: this.fileName, _id: 'oldFileRef' } + this.ProjectEntityMongoUpdateHandler._confirmFolder = sinon + .stub() + .yields(folder_id) + this.ProjectEntityMongoUpdateHandler._putElement = sinon + .stub() + .yields(null, { path: { fileSystem: this.fileSystemPath } }) + this.FileStoreHandler.copyFile = sinon.stub().yields(null, this.fileUrl) + return this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject( + this.project._id, + this.project, + folder_id, + this.oldProject_id, + this.oldFileRef, + userId, + this.callback + ) + }) + + it('should copy the file in FileStoreHandler', function() { + return this.FileStoreHandler.copyFile + .calledWith( + this.oldProject_id, + this.oldFileRef._id, + project_id, + file_id + ) + .should.equal(true) + }) + + it('should put file into folder by calling put element', function() { + return this.ProjectEntityMongoUpdateHandler._putElement + .calledWithMatch( + this.project, + folder_id, + { _id: file_id, name: this.fileName }, + 'file' + ) + .should.equal(true) + }) + + it('should return doc and parent folder', function() { + return this.callback + .calledWithMatch(null, { _id: file_id, name: this.fileName }, folder_id) + .should.equal(true) + }) + + it('should call third party data store if versioning is enabled', function() { + return this.TpdsUpdateSender.addFile + .calledWith({ + project_id, + file_id, + path: this.fileSystemPath, + rev: 0, + project_name: this.project.name + }) + .should.equal(true) + }) + + return it('should should send the change in project structure to the doc updater', function() { + const changesMatcher = sinon.match(changes => { + const { newFiles } = changes + if (newFiles.length !== 1) { + return false + } + const newFile = newFiles[0] + return ( + newFile.file._id === file_id && + newFile.path === this.fileSystemPath && + newFile.url === this.fileUrl + ) + }) + + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWithMatch(project_id, projectHistoryId, userId, changesMatcher) + .should.equal(true) + }) + }) + + describe('copyFileFromExistingProjectWithProject, with linkedFileData and hash', function() { + beforeEach(function() { + this.oldProject_id = '123kljadas' + this.oldFileRef = { + _id: 'oldFileRef', + name: this.fileName, + linkedFileData: this.linkedFileData, + hash: '123456' + } + this.ProjectEntityMongoUpdateHandler._confirmFolder = sinon + .stub() + .yields(folder_id) + this.ProjectEntityMongoUpdateHandler._putElement = sinon + .stub() + .yields(null, { path: { fileSystem: this.fileSystemPath } }) + this.FileStoreHandler.copyFile = sinon.stub().yields(null, this.fileUrl) + return this.ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject( + this.project._id, + this.project, + folder_id, + this.oldProject_id, + this.oldFileRef, + userId, + this.callback + ) + }) + + it('should copy the file in FileStoreHandler', function() { + return this.FileStoreHandler.copyFile + .calledWith( + this.oldProject_id, + this.oldFileRef._id, + project_id, + file_id + ) + .should.equal(true) + }) + + return it('should put file into folder by calling put element, with the linkedFileData and hash', function() { + return this.ProjectEntityMongoUpdateHandler._putElement + .calledWithMatch( + this.project, + folder_id, + { + _id: file_id, + name: this.fileName, + linkedFileData: this.linkedFileData, + hash: '123456' + }, + 'file' + ) + .should.equal(true) + }) + }) + + describe('updateDocLines', function() { + beforeEach(function() { + this.path = '/somewhere/something.tex' + this.doc = { + _id: doc_id + } + this.version = 42 + this.ranges = { mock: 'ranges' } + this.lastUpdatedAt = new Date().getTime() + this.lastUpdatedBy = 'fake-last-updater-id' + this.ProjectGetter.getProjectWithoutDocLines = sinon + .stub() + .yields(null, this.project) + this.ProjectLocator.findElement = sinon + .stub() + .yields(null, this.doc, { fileSystem: this.path }) + this.TpdsUpdateSender.addDoc = sinon.stub().yields() + this.ProjectUpdater.markAsUpdated = sinon.stub() + return (this.callback = sinon.stub()) + }) + + describe('when the doc has been modified', function() { + beforeEach(function() { + this.DocstoreManager.updateDoc = sinon + .stub() + .yields(null, true, (this.rev = 5)) + return this.ProjectEntityUpdateHandler.updateDocLines( + project_id, + doc_id, + this.docLines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) + + it('should get the project without doc lines', function() { + return this.ProjectGetter.getProjectWithoutDocLines + .calledWith(project_id) + .should.equal(true) + }) + + it('should find the doc', function() { + return this.ProjectLocator.findElement + .calledWith({ + project: this.project, + type: 'docs', + element_id: doc_id + }) + .should.equal(true) + }) + + it('should update the doc in the docstore', function() { + return this.DocstoreManager.updateDoc + .calledWith( + project_id, + doc_id, + this.docLines, + this.version, + this.ranges + ) + .should.equal(true) + }) + + it('should mark the project as updated', function() { + return sinon.assert.calledWith( + this.ProjectUpdater.markAsUpdated, + project_id, + this.lastUpdatedAt, + this.lastUpdatedBy + ) + }) + + it('should send the doc the to the TPDS', function() { + return this.TpdsUpdateSender.addDoc + .calledWith({ + project_id, + project_name: this.project.name, + doc_id, + rev: this.rev, + path: this.path + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when the doc has not been modified', function() { + beforeEach(function() { + this.DocstoreManager.updateDoc = sinon + .stub() + .yields(null, false, (this.rev = 5)) + return this.ProjectEntityUpdateHandler.updateDocLines( + project_id, + doc_id, + this.docLines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) + + it('should not mark the project as updated', function() { + return this.ProjectUpdater.markAsUpdated.called.should.equal(false) + }) + + it('should not send the doc the to the TPDS', function() { + return this.TpdsUpdateSender.addDoc.called.should.equal(false) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when the doc has been deleted', function() { + beforeEach(function() { + this.project.deletedDocs = [{ _id: doc_id }] + this.ProjectGetter.getProjectWithoutDocLines = sinon + .stub() + .yields(null, this.project) + this.ProjectLocator.findElement = sinon + .stub() + .yields(new Errors.NotFoundError()) + this.DocstoreManager.updateDoc = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.updateDocLines( + project_id, + doc_id, + this.docLines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) + + it('should update the doc in the docstore', function() { + return this.DocstoreManager.updateDoc + .calledWith( + project_id, + doc_id, + this.docLines, + this.version, + this.ranges + ) + .should.equal(true) + }) + + it('should not mark the project as updated', function() { + return this.ProjectUpdater.markAsUpdated.called.should.equal(false) + }) + + it('should not send the doc the to the TPDS', function() { + return this.TpdsUpdateSender.addDoc.called.should.equal(false) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when the doc is not related to the project', function() { + beforeEach(function() { + this.ProjectLocator.findElement = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.updateDocLines( + project_id, + doc_id, + this.docLines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) + + it('should log out the error', function() { + return this.logger.error + .calledWith( + { + project_id, + doc_id, + lines: this.docLines + }, + 'doc not found while updating doc lines' + ) + .should.equal(true) + }) + + return it('should return a not found error', function() { + return this.callback + .calledWith(new Errors.NotFoundError()) + .should.equal(true) + }) + }) + + return describe('when the project is not found', function() { + beforeEach(function() { + this.ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.updateDocLines( + project_id, + doc_id, + this.docLines, + this.version, + this.ranges, + this.lastUpdatedAt, + this.lastUpdatedBy, + this.callback + ) + }) + + return it('should return a not found error', function() { + return this.callback + .calledWith(new Errors.NotFoundError()) + .should.equal(true) + }) + }) + }) + + describe('setRootDoc', () => + it('should call Project.update', function() { + const rootDoc_id = 'root-doc-id-123123' + this.ProjectModel.update = sinon.stub() + this.ProjectEntityUpdateHandler.setRootDoc(project_id, rootDoc_id) + return this.ProjectModel.update + .calledWith({ _id: project_id }, { rootDoc_id }) + .should.equal(true) + })) + + describe('unsetRootDoc', () => + it('should call Project.update', function() { + this.ProjectModel.update = sinon.stub() + this.ProjectEntityUpdateHandler.unsetRootDoc(project_id) + return this.ProjectModel.update + .calledWith({ _id: project_id }, { $unset: { rootDoc_id: true } }) + .should.equal(true) + })) + + describe('addDoc', function() { + describe('adding a doc', function() { + beforeEach(function() { + this.path = '/path/to/doc' + + this.newDoc = { + name: this.docName, + lines: undefined, + _id: doc_id, + rev: 0 + } + this.DocstoreManager.updateDoc = sinon + .stub() + .yields(null, false, (this.rev = 5)) + this.TpdsUpdateSender.addDoc = sinon.stub().yields() + this.ProjectEntityMongoUpdateHandler.addDoc = sinon + .stub() + .yields(null, { path: { fileSystem: this.path } }, this.project) + return this.ProjectEntityUpdateHandler.addDoc( + project_id, + doc_id, + this.docName, + this.docLines, + userId, + this.callback + ) + }) + + it('creates the doc without history', function() { + return this.DocstoreManager.updateDoc + .calledWith(project_id, doc_id, this.docLines, 0, {}) + .should.equal(true) + }) + + return it('sends the change in project structure to the doc updater', function() { + const newDocs = [ + { + doc: this.newDoc, + path: this.path, + docLines: this.docLines.join('\n') + } + ] + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, projectHistoryId, userId, { + newDocs, + newProject: this.project + }) + .should.equal(true) + }) + }) + + return describe('adding a doc with an invalid name', function() { + beforeEach(function() { + this.path = '/path/to/doc' + + this.newDoc = { _id: doc_id } + return this.ProjectEntityUpdateHandler.addDoc( + project_id, + folder_id, + `*${this.docName}`, + this.docLines, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('addFile', function() { + describe('adding a file', function() { + beforeEach(function() { + this.path = '/path/to/file' + + this.newFile = { + _id: file_id, + rev: 0, + name: this.fileName, + linkedFileData: this.linkedFileData + } + this.FileStoreHandler.uploadFileFromDisk = sinon + .stub() + .yields(null, this.fileUrl, this.newFile) + this.TpdsUpdateSender.addFile = sinon.stub().yields() + this.ProjectEntityMongoUpdateHandler.addFile = sinon + .stub() + .yields(null, { path: { fileSystem: this.path } }, this.project) + return this.ProjectEntityUpdateHandler.addFile( + project_id, + folder_id, + this.fileName, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + it('updates the file in the filestore', function() { + return this.FileStoreHandler.uploadFileFromDisk + .calledWith( + project_id, + { name: this.fileName, linkedFileData: this.linkedFileData }, + this.fileSystemPath + ) + .should.equal(true) + }) + + it('updates the file in mongo', function() { + const fileMatcher = sinon.match(file => { + return file.name === this.fileName + }) + + return this.ProjectEntityMongoUpdateHandler.addFile + .calledWithMatch(project_id, folder_id, fileMatcher) + .should.equal(true) + }) + + it('notifies the tpds', function() { + return this.TpdsUpdateSender.addFile + .calledWith({ + project_id, + project_name: this.project.name, + file_id, + rev: 0, + path: this.path + }) + .should.equal(true) + }) + + return it('sends the change in project structure to the doc updater', function() { + const newFiles = [ + { + file: this.newFile, + path: this.path, + url: this.fileUrl + } + ] + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, projectHistoryId, userId, { + newFiles, + newProject: this.project + }) + .should.equal(true) + }) + }) + + return describe('adding a file with an invalid name', function() { + beforeEach(function() { + this.path = '/path/to/file' + + this.newFile = { + _id: file_id, + rev: 0, + name: this.fileName, + linkedFileData: this.linkedFileData + } + this.TpdsUpdateSender.addFile = sinon.stub().yields() + this.ProjectEntityMongoUpdateHandler.addFile = sinon + .stub() + .yields(null, { path: { fileSystem: this.path } }, this.project) + return this.ProjectEntityUpdateHandler.addFile( + project_id, + folder_id, + `*${this.fileName}`, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('replaceFile', function() { + beforeEach(function() { + // replacement file now creates a new file object + this.newFileUrl = 'new-file-url' + this.FileStoreHandler.uploadFileFromDisk = sinon + .stub() + .yields(null, this.newFileUrl, this.newFile) + + this.newFile = { + _id: new_file_id, + name: 'dummy-upload-filename', + rev: 0, + linkedFileData: this.linkedFileData + } + this.oldFile = { _id: file_id, rev: 3 } + this.path = '/path/to/file' + this.newProject = 'new project' + this.FileStoreHandler.uploadFileFromDisk = sinon + .stub() + .yields(null, this.newFileUrl, this.newFile) + this.ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon + .stub() + .yields() + this.ProjectEntityMongoUpdateHandler.replaceFileWithNew = sinon + .stub() + .yields( + null, + this.oldFile, + this.project, + { fileSystem: this.path }, + this.newProject + ) + return this.ProjectEntityUpdateHandler.replaceFile( + project_id, + file_id, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + it('uploads a new version of the file', function() { + return this.FileStoreHandler.uploadFileFromDisk + .calledWith( + project_id, + { + name: 'dummy-upload-filename', + linkedFileData: this.linkedFileData + }, + this.fileSystemPath + ) + .should.equal(true) + }) + + it('replaces the file in mongo', function() { + return this.ProjectEntityMongoUpdateHandler.replaceFileWithNew + .calledWith(project_id, file_id, this.newFile) + .should.equal(true) + }) + + it('notifies the tpds', function() { + return this.TpdsUpdateSender.addFile + .calledWith({ + project_id, + project_name: this.project.name, + file_id: new_file_id, + rev: this.oldFile.rev + 1, + path: this.path + }) + .should.equal(true) + }) + + return it('updates the project structure in the doc updater', function() { + const oldFiles = [ + { + file: this.oldFile, + path: this.path + } + ] + const newFiles = [ + { + file: this.newFile, + path: this.path, + url: this.newFileUrl + } + ] + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, projectHistoryId, userId, { + oldFiles, + newFiles, + newProject: this.newProject + }) + .should.equal(true) + }) + }) + + describe('upsertDoc', function() { + describe('upserting into an invalid folder', function() { + beforeEach(function() { + this.ProjectLocator.findElement = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.upsertDoc( + project_id, + folder_id, + this.docName, + this.docLines, + this.source, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Error) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + + describe('updating an existing doc', function() { + beforeEach(function() { + this.existingDoc = { _id: doc_id, name: this.docName } + this.folder = { _id: folder_id, docs: [this.existingDoc] } + this.ProjectLocator.findElement = sinon.stub().yields(null, this.folder) + this.DocumentUpdaterHandler.setDocument = sinon.stub().yields() + this.DocumentUpdaterHandler.flushDocToMongo = sinon.stub().yields() + + return this.ProjectEntityUpdateHandler.upsertDoc( + project_id, + folder_id, + this.docName, + this.docLines, + this.source, + userId, + this.callback + ) + }) + + it('tries to find the folder', function() { + return this.ProjectLocator.findElement + .calledWith({ project_id, element_id: folder_id, type: 'folder' }) + .should.equal(true) + }) + + it('updates the doc contents', function() { + return this.DocumentUpdaterHandler.setDocument + .calledWith( + project_id, + this.existingDoc._id, + userId, + this.docLines, + this.source + ) + .should.equal(true) + }) + + it('flushes the doc contents', function() { + return this.DocumentUpdaterHandler.flushDocToMongo + .calledWith(project_id, this.existingDoc._id) + .should.equal(true) + }) + + return it('returns the doc', function() { + return this.callback.calledWith(null, this.existingDoc, false) + }) + }) + + describe('creating a new doc', function() { + beforeEach(function() { + this.folder = { _id: folder_id, docs: [] } + this.newDoc = { _id: doc_id } + this.ProjectLocator.findElement = sinon.stub().yields(null, this.folder) + this.ProjectEntityUpdateHandler.addDocWithRanges = { + withoutLock: sinon.stub().yields(null, this.newDoc) + } + + return this.ProjectEntityUpdateHandler.upsertDoc( + project_id, + folder_id, + this.docName, + this.docLines, + this.source, + userId, + this.callback + ) + }) + + it('tries to find the folder', function() { + return this.ProjectLocator.findElement + .calledWith({ project_id, element_id: folder_id, type: 'folder' }) + .should.equal(true) + }) + + it('adds the doc', function() { + return this.ProjectEntityUpdateHandler.addDocWithRanges.withoutLock + .calledWith( + project_id, + folder_id, + this.docName, + this.docLines, + {}, + userId + ) + .should.equal(true) + }) + + return it('returns the doc', function() { + return this.callback.calledWith(null, this.newDoc, true) + }) + }) + + return describe('upserting a new doc with an invalid name', function() { + beforeEach(function() { + this.folder = { _id: folder_id, docs: [] } + this.newDoc = { _id: doc_id } + this.ProjectLocator.findElement = sinon.stub().yields(null, this.folder) + this.ProjectEntityUpdateHandler.addDocWithRanges = { + withoutLock: sinon.stub().yields(null, this.newDoc) + } + + return this.ProjectEntityUpdateHandler.upsertDoc( + project_id, + folder_id, + `*${this.docName}`, + this.docLines, + this.source, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('upsertFile', function() { + beforeEach(function() { + return (this.FileStoreHandler.uploadFileFromDisk = sinon + .stub() + .yields(null, this.fileUrl, this.newFile)) + }) + + describe('upserting into an invalid folder', function() { + beforeEach(function() { + this.ProjectLocator.findElement = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.upsertFile( + project_id, + folder_id, + this.fileName, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Error) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + + describe('updating an existing file', function() { + beforeEach(function() { + this.existingFile = { _id: file_id, name: this.fileName } + this.folder = { _id: folder_id, fileRefs: [this.existingFile] } + this.ProjectLocator.findElement = sinon.stub().yields(null, this.folder) + this.ProjectEntityUpdateHandler.replaceFile = { + mainTask: sinon.stub().yields(null, this.newFile) + } + + return this.ProjectEntityUpdateHandler.upsertFile( + project_id, + folder_id, + this.fileName, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + it('replaces the file', function() { + return this.ProjectEntityUpdateHandler.replaceFile.mainTask + .calledWith( + project_id, + file_id, + this.fileSystemPath, + this.linkedFileData, + userId + ) + .should.equal(true) + }) + + return it('returns the file', function() { + return this.callback.calledWith(null, this.existingFile, false) + }) + }) + + describe('creating a new file', function() { + beforeEach(function() { + this.folder = { _id: folder_id, fileRefs: [] } + this.newFile = { _id: file_id } + this.ProjectLocator.findElement = sinon.stub().yields(null, this.folder) + this.ProjectEntityUpdateHandler.addFile = { + mainTask: sinon.stub().yields(null, this.newFile) + } + + return this.ProjectEntityUpdateHandler.upsertFile( + project_id, + folder_id, + this.fileName, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + it('tries to find the folder', function() { + return this.ProjectLocator.findElement + .calledWith({ project_id, element_id: folder_id, type: 'folder' }) + .should.equal(true) + }) + + it('adds the file', function() { + return this.ProjectEntityUpdateHandler.addFile.mainTask + .calledWith( + project_id, + folder_id, + this.fileName, + this.fileSystemPath, + this.linkedFileData, + userId + ) + .should.equal(true) + }) + + return it('returns the file', function() { + return this.callback.calledWith(null, this.newFile, true) + }) + }) + + return describe('upserting a new file with an invalid name', function() { + beforeEach(function() { + this.folder = { _id: folder_id, fileRefs: [] } + this.newFile = { _id: file_id } + this.ProjectLocator.findElement = sinon.stub().yields(null, this.folder) + this.ProjectEntityUpdateHandler.addFile = { + mainTask: sinon.stub().yields(null, this.newFile) + } + + return this.ProjectEntityUpdateHandler.upsertFile( + project_id, + folder_id, + `*${this.fileName}`, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('upsertDocWithPath', function() { + describe('upserting a doc', function() { + beforeEach(function() { + this.path = '/folder/doc.tex' + this.newFolders = ['mock-a', 'mock-b'] + this.folder = { _id: folder_id } + this.doc = { _id: doc_id } + this.isNewDoc = true + this.ProjectEntityUpdateHandler.mkdirp = { + withoutLock: sinon.stub().yields(null, this.newFolders, this.folder) + } + this.ProjectEntityUpdateHandler.upsertDoc = { + withoutLock: sinon.stub().yields(null, this.doc, this.isNewDoc) + } + + return this.ProjectEntityUpdateHandler.upsertDocWithPath( + project_id, + this.path, + this.docLines, + this.source, + userId, + this.callback + ) + }) + + it('creates any necessary folders', function() { + return this.ProjectEntityUpdateHandler.mkdirp.withoutLock + .calledWith(project_id, '/folder') + .should.equal(true) + }) + + it('upserts the doc', function() { + return this.ProjectEntityUpdateHandler.upsertDoc.withoutLock + .calledWith( + project_id, + this.folder._id, + 'doc.tex', + this.docLines, + this.source, + userId + ) + .should.equal(true) + }) + + return it('calls the callback', function() { + return this.callback + .calledWith( + null, + this.doc, + this.isNewDoc, + this.newFolders, + this.folder + ) + .should.equal(true) + }) + }) + + describe('upserting a doc with an invalid path', function() { + beforeEach(function() { + this.path = '/*folder/doc.tex' + this.newFolders = ['mock-a', 'mock-b'] + this.folder = { _id: folder_id } + this.doc = { _id: doc_id } + this.isNewDoc = true + this.ProjectEntityUpdateHandler.mkdirp = { + withoutLock: sinon.stub().yields(null, this.newFolders, this.folder) + } + this.ProjectEntityUpdateHandler.upsertDoc = { + withoutLock: sinon.stub().yields(null, this.doc, this.isNewDoc) + } + + return this.ProjectEntityUpdateHandler.upsertDocWithPath( + project_id, + this.path, + this.docLines, + this.source, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + + return describe('upserting a doc with an invalid name', function() { + beforeEach(function() { + this.path = '/folder/*doc.tex' + this.newFolders = ['mock-a', 'mock-b'] + this.folder = { _id: folder_id } + this.doc = { _id: doc_id } + this.isNewDoc = true + this.ProjectEntityUpdateHandler.mkdirp = { + withoutLock: sinon.stub().yields(null, this.newFolders, this.folder) + } + this.ProjectEntityUpdateHandler.upsertDoc = { + withoutLock: sinon.stub().yields(null, this.doc, this.isNewDoc) + } + + return this.ProjectEntityUpdateHandler.upsertDocWithPath( + project_id, + this.path, + this.docLines, + this.source, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('upsertFileWithPath', function() { + describe('upserting a file', function() { + beforeEach(function() { + this.path = '/folder/file.png' + this.newFolders = ['mock-a', 'mock-b'] + this.folder = { _id: folder_id } + this.file = { _id: file_id } + this.isNewFile = true + this.FileStoreHandler.uploadFileFromDisk = sinon + .stub() + .yields(null, this.fileUrl, this.newFile) + this.ProjectEntityUpdateHandler.mkdirp = { + withoutLock: sinon.stub().yields(null, this.newFolders, this.folder) + } + this.ProjectEntityUpdateHandler.upsertFile = { + mainTask: sinon.stub().yields(null, this.file, this.isNewFile) + } + + return this.ProjectEntityUpdateHandler.upsertFileWithPath( + project_id, + this.path, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + it('creates any necessary folders', function() { + return this.ProjectEntityUpdateHandler.mkdirp.withoutLock + .calledWith(project_id, '/folder') + .should.equal(true) + }) + + it('upserts the file', function() { + return this.ProjectEntityUpdateHandler.upsertFile.mainTask + .calledWith( + project_id, + this.folder._id, + 'file.png', + this.fileSystemPath, + this.linkedFileData, + userId + ) + .should.equal(true) + }) + + return it('calls the callback', function() { + return this.callback + .calledWith( + null, + this.file, + this.isNewFile, + undefined, + this.newFolders, + this.folder + ) + .should.equal(true) + }) + }) + + describe('upserting a file with an invalid path', function() { + beforeEach(function() { + this.path = '/*folder/file.png' + this.newFolders = ['mock-a', 'mock-b'] + this.folder = { _id: folder_id } + this.file = { _id: file_id } + this.isNewFile = true + this.ProjectEntityUpdateHandler.mkdirp = { + withoutLock: sinon.stub().yields(null, this.newFolders, this.folder) + } + this.ProjectEntityUpdateHandler.upsertFile = { + mainTask: sinon.stub().yields(null, this.file, this.isNewFile) + } + + return this.ProjectEntityUpdateHandler.upsertFileWithPath( + project_id, + this.path, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + + return describe('upserting a file with an invalid name', function() { + beforeEach(function() { + this.path = '/folder/*file.png' + this.newFolders = ['mock-a', 'mock-b'] + this.folder = { _id: folder_id } + this.file = { _id: file_id } + this.isNewFile = true + this.ProjectEntityUpdateHandler.mkdirp = { + withoutLock: sinon.stub().yields(null, this.newFolders, this.folder) + } + this.ProjectEntityUpdateHandler.upsertFile = { + mainTask: sinon.stub().yields(null, this.file, this.isNewFile) + } + + return this.ProjectEntityUpdateHandler.upsertFileWithPath( + project_id, + this.path, + this.fileSystemPath, + this.linkedFileData, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('deleteEntity', function() { + beforeEach(function() { + this.path = '/path/to/doc.tex' + this.doc = { _id: doc_id } + this.projectBeforeDeletion = { _id: project_id, name: 'project' } + this.newProject = 'new-project' + this.ProjectEntityMongoUpdateHandler.deleteEntity = sinon + .stub() + .yields( + null, + this.doc, + { fileSystem: this.path }, + this.projectBeforeDeletion, + this.newProject + ) + this.ProjectEntityUpdateHandler._cleanUpEntity = sinon.stub().yields() + this.TpdsUpdateSender.deleteEntity = sinon.stub().yields() + + return this.ProjectEntityUpdateHandler.deleteEntity( + project_id, + doc_id, + 'doc', + userId, + this.callback + ) + }) + + it('deletes the entity in mongo', function() { + return this.ProjectEntityMongoUpdateHandler.deleteEntity + .calledWith(project_id, doc_id, 'doc') + .should.equal(true) + }) + + it('cleans up the doc in the docstore', function() { + return this.ProjectEntityUpdateHandler._cleanUpEntity + .calledWith( + this.projectBeforeDeletion, + this.newProject, + this.doc, + 'doc', + this.path, + userId + ) + .should.equal(true) + }) + + it('it notifies the tpds', function() { + return this.TpdsUpdateSender.deleteEntity + .calledWith({ + project_id, + path: this.path, + project_name: this.projectBeforeDeletion.name + }) + .should.equal(true) + }) + + return it('retuns the entity_id', function() { + return this.callback.calledWith(null, doc_id).should.equal(true) + }) + }) + + describe('deleteEntityWithPath', function() { + describe('when the entity exists', function() { + beforeEach(function() { + this.doc = { _id: doc_id } + this.ProjectLocator.findElementByPath = sinon + .stub() + .yields(null, this.doc, 'doc') + this.ProjectEntityUpdateHandler.deleteEntity = { + withoutLock: sinon.stub().yields() + } + this.path = '/path/to/doc.tex' + return this.ProjectEntityUpdateHandler.deleteEntityWithPath( + project_id, + this.path, + userId, + this.callback + ) + }) + + it('finds the entity', function() { + return this.ProjectLocator.findElementByPath + .calledWith({ project_id, path: this.path }) + .should.equal(true) + }) + + return it('deletes the entity', function() { + return this.ProjectEntityUpdateHandler.deleteEntity.withoutLock + .calledWith(project_id, this.doc._id, 'doc', userId, this.callback) + .should.equal(true) + }) + }) + + return describe('when the entity does not exist', function() { + beforeEach(function() { + this.ProjectLocator.findElementByPath = sinon.stub().yields() + this.path = '/doc.tex' + return this.ProjectEntityUpdateHandler.deleteEntityWithPath( + project_id, + this.path, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + return this.callback + .calledWith(new Errors.NotFoundError()) + .should.equal(true) + }) + }) + }) + + describe('mkdirp', function() { + beforeEach(function() { + this.docPath = '/folder/doc.tex' + this.ProjectEntityMongoUpdateHandler.mkdirp = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.mkdirp( + project_id, + this.docPath, + this.callback + ) + }) + + return it('calls ProjectEntityMongoUpdateHandler', function() { + return this.ProjectEntityMongoUpdateHandler.mkdirp + .calledWith(project_id, this.docPath) + .should.equal(true) + }) + }) + + describe('mkdirpWithExactCase', function() { + beforeEach(function() { + this.docPath = '/folder/doc.tex' + this.ProjectEntityMongoUpdateHandler.mkdirp = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.mkdirpWithExactCase( + project_id, + this.docPath, + this.callback + ) + }) + + return it('calls ProjectEntityMongoUpdateHandler', function() { + return this.ProjectEntityMongoUpdateHandler.mkdirp + .calledWith(project_id, this.docPath, { exactCaseMatch: true }) + .should.equal(true) + }) + }) + + describe('addFolder', function() { + describe('adding a folder', function() { + beforeEach(function() { + this.parentFolder_id = '123asdf' + this.folderName = 'new-folder' + this.ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.addFolder( + project_id, + this.parentFolder_id, + this.folderName, + this.callback + ) + }) + + return it('calls ProjectEntityMongoUpdateHandler', function() { + return this.ProjectEntityMongoUpdateHandler.addFolder + .calledWith(project_id, this.parentFolder_id, this.folderName) + .should.equal(true) + }) + }) + + return describe('adding a folder with an invalid name', function() { + beforeEach(function() { + this.parentFolder_id = '123asdf' + this.folderName = '*new-folder' + this.ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields() + return this.ProjectEntityUpdateHandler.addFolder( + project_id, + this.parentFolder_id, + this.folderName, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('moveEntity', function() { + beforeEach(function() { + this.project_name = 'project name' + this.startPath = '/a.tex' + this.endPath = '/folder/b.tex' + this.rev = 2 + this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } + this.ProjectEntityMongoUpdateHandler.moveEntity = sinon + .stub() + .yields( + null, + this.project, + this.startPath, + this.endPath, + this.rev, + this.changes + ) + this.TpdsUpdateSender.moveEntity = sinon.stub() + this.DocumentUpdaterHandler.updateProjectStructure = sinon.stub() + + return this.ProjectEntityUpdateHandler.moveEntity( + project_id, + doc_id, + folder_id, + 'doc', + userId, + this.callback + ) + }) + + it('moves the entity in mongo', function() { + return this.ProjectEntityMongoUpdateHandler.moveEntity + .calledWith(project_id, doc_id, folder_id, 'doc') + .should.equal(true) + }) + + it('notifies tpds', function() { + return this.TpdsUpdateSender.moveEntity + .calledWith({ + project_id, + project_name: this.project_name, + startPath: this.startPath, + endPath: this.endPath, + rev: this.rev + }) + .should.equal(true) + }) + + return it('sends the changes in project structure to the doc updater', function() { + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith( + project_id, + projectHistoryId, + userId, + this.changes, + this.callback + ) + .should.equal(true) + }) + }) + + describe('renameEntity', function() { + describe('renaming an entity', function() { + beforeEach(function() { + this.project_name = 'project name' + this.startPath = '/folder/a.tex' + this.endPath = '/folder/b.tex' + this.rev = 2 + this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } + this.newDocName = 'b.tex' + this.ProjectEntityMongoUpdateHandler.renameEntity = sinon + .stub() + .yields( + null, + this.project, + this.startPath, + this.endPath, + this.rev, + this.changes + ) + this.TpdsUpdateSender.moveEntity = sinon.stub() + this.DocumentUpdaterHandler.updateProjectStructure = sinon.stub() + + return this.ProjectEntityUpdateHandler.renameEntity( + project_id, + doc_id, + 'doc', + this.newDocName, + userId, + this.callback + ) + }) + + it('moves the entity in mongo', function() { + return this.ProjectEntityMongoUpdateHandler.renameEntity + .calledWith(project_id, doc_id, 'doc', this.newDocName) + .should.equal(true) + }) + + it('notifies tpds', function() { + return this.TpdsUpdateSender.moveEntity + .calledWith({ + project_id, + project_name: this.project_name, + startPath: this.startPath, + endPath: this.endPath, + rev: this.rev + }) + .should.equal(true) + }) + + return it('sends the changes in project structure to the doc updater', function() { + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith( + project_id, + projectHistoryId, + userId, + this.changes, + this.callback + ) + .should.equal(true) + }) + }) + + return describe('renaming an entity to an invalid name', function() { + beforeEach(function() { + this.project_name = 'project name' + this.startPath = '/folder/a.tex' + this.endPath = '/folder/b.tex' + this.rev = 2 + this.changes = { newDocs: ['old-doc'], newFiles: ['old-file'] } + this.newDocName = '*b.tex' + this.ProjectEntityMongoUpdateHandler.renameEntity = sinon + .stub() + .yields( + null, + this.project, + this.startPath, + this.endPath, + this.rev, + this.changes + ) + this.TpdsUpdateSender.moveEntity = sinon.stub() + this.DocumentUpdaterHandler.updateProjectStructure = sinon.stub() + + return this.ProjectEntityUpdateHandler.renameEntity( + project_id, + doc_id, + 'doc', + this.newDocName, + userId, + this.callback + ) + }) + + return it('returns an error', function() { + const errorMatcher = sinon.match.instanceOf(Errors.InvalidNameError) + return this.callback.calledWithMatch(errorMatcher).should.equal(true) + }) + }) + }) + + describe('resyncProjectHistory', function() { + describe('a deleted project', function() { + beforeEach(function() { + this.ProjectGetter.getProject = sinon.stub().yields() + + return this.ProjectEntityUpdateHandler.resyncProjectHistory( + project_id, + this.callback + ) + }) + + return it('should return an error', function() { + const error = new Errors.ProjectHistoryDisabledError( + `project history not enabled for ${project_id}` + ) + return this.callback.calledWith(error).should.equal(true) + }) + }) + + describe('a project without project-history enabled', function() { + beforeEach(function() { + this.project.overleaf = {} + this.ProjectGetter.getProject = sinon.stub().yields(null, this.project) + + return this.ProjectEntityUpdateHandler.resyncProjectHistory( + project_id, + this.callback + ) + }) + + return it('should return an error', function() { + const error = new Errors.ProjectHistoryDisabledError( + `project history not enabled for ${project_id}` + ) + return this.callback.calledWith(error).should.equal(true) + }) + }) + + return describe('a project with project-history enabled', function() { + beforeEach(function() { + this.ProjectGetter.getProject = sinon.stub().yields(null, this.project) + const docs = [ + { + doc: { + _id: doc_id + }, + path: 'main.tex' + } + ] + const files = [ + { + file: { + _id: file_id + }, + path: 'universe.png' + } + ] + this.ProjectEntityHandler.getAllEntitiesFromProject = sinon + .stub() + .yields(null, docs, files) + this.FileStoreHandler._buildUrl = (project_id, file_id) => + `www.filestore.test/${project_id}/${file_id}` + this.DocumentUpdaterHandler.resyncProjectHistory = sinon.stub().yields() + + return this.ProjectEntityUpdateHandler.resyncProjectHistory( + project_id, + this.callback + ) + }) + + it('gets the project', function() { + return this.ProjectGetter.getProject + .calledWith(project_id) + .should.equal(true) + }) + + it('gets the entities for the project', function() { + return this.ProjectEntityHandler.getAllEntitiesFromProject + .calledWith(this.project) + .should.equal(true) + }) + + it('tells the doc updater to sync the project', function() { + const docs = [ + { + doc: doc_id, + path: 'main.tex' + } + ] + const files = [ + { + file: file_id, + path: 'universe.png', + url: `www.filestore.test/${project_id}/${file_id}` + } + ] + return this.DocumentUpdaterHandler.resyncProjectHistory + .calledWith(project_id, projectHistoryId, docs, files) + .should.equal(true) + }) + + return it('calls the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('_cleanUpEntity', function() { + beforeEach(function() { + this.entity_id = '4eecaffcbffa66588e000009' + this.FileStoreHandler.deleteFile = sinon.stub().yields() + this.DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() + this.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() + return (this.ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon + .stub() + .yields()) + }) + + describe('a file', function() { + beforeEach(function(done) { + this.path = '/file/system/path.png' + this.entity = { _id: this.entity_id } + this.newProject = 'new-project' + return this.ProjectEntityUpdateHandler._cleanUpEntity( + this.project, + this.newProject, + this.entity, + 'file', + this.path, + userId, + done + ) + }) + + it('should insert the file into the deletedFiles array', function() { + return this.ProjectEntityMongoUpdateHandler._insertDeletedFileReference + .calledWith(this.project._id, this.entity) + .should.equal(true) + }) + + it('should not delete the file from FileStoreHandler', function() { + return this.FileStoreHandler.deleteFile + .calledWith(project_id, this.entity_id) + .should.equal(false) + }) + + it('should not attempt to delete from the document updater', function() { + return this.DocumentUpdaterHandler.deleteDoc.called.should.equal(false) + }) + + return it('should should send the update to the doc updater', function() { + const oldFiles = [{ file: this.entity, path: this.path }] + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, projectHistoryId, userId, { + oldFiles, + newProject: this.newProject + }) + .should.equal(true) + }) + }) + + describe('a doc', function() { + beforeEach(function(done) { + this.path = '/file/system/path.tex' + this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields() + this.entity = { _id: this.entity_id } + this.newProject = 'new-project' + return this.ProjectEntityUpdateHandler._cleanUpEntity( + this.project, + this.newProject, + this.entity, + 'doc', + this.path, + userId, + done + ) + }) + + it('should clean up the doc', function() { + return this.ProjectEntityUpdateHandler._cleanUpDoc + .calledWith(this.project, this.entity, this.path, userId) + .should.equal(true) + }) + + return it('should should send the update to the doc updater', function() { + const oldDocs = [{ doc: this.entity, path: this.path }] + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, projectHistoryId, userId, { + oldDocs, + newProject: this.newProject + }) + .should.equal(true) + }) + }) + + return describe('a folder', function() { + beforeEach(function(done) { + this.folder = { + folders: [ + { + name: 'subfolder', + fileRefs: [ + (this.file1 = { _id: 'file-id-1', name: 'file-name-1' }) + ], + docs: [(this.doc1 = { _id: 'doc-id-1', name: 'doc-name-1' })], + folders: [] + } + ], + fileRefs: [(this.file2 = { _id: 'file-id-2', name: 'file-name-2' })], + docs: [(this.doc2 = { _id: 'doc-id-2', name: 'doc-name-2' })] + } + + this.ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields() + this.ProjectEntityUpdateHandler._cleanUpFile = sinon.stub().yields() + const path = '/folder' + this.newProject = 'new-project' + return this.ProjectEntityUpdateHandler._cleanUpEntity( + this.project, + this.newProject, + this.folder, + 'folder', + path, + userId, + done + ) + }) + + it('should clean up all sub files', function() { + this.ProjectEntityUpdateHandler._cleanUpFile + .calledWith( + this.project, + this.file1, + '/folder/subfolder/file-name-1', + userId + ) + .should.equal(true) + return this.ProjectEntityUpdateHandler._cleanUpFile + .calledWith(this.project, this.file2, '/folder/file-name-2', userId) + .should.equal(true) + }) + + it('should clean up all sub docs', function() { + this.ProjectEntityUpdateHandler._cleanUpDoc + .calledWith( + this.project, + this.doc1, + '/folder/subfolder/doc-name-1', + userId + ) + .should.equal(true) + return this.ProjectEntityUpdateHandler._cleanUpDoc + .calledWith(this.project, this.doc2, '/folder/doc-name-2', userId) + .should.equal(true) + }) + + return it('should should send one update to the doc updater for all docs and files', function() { + const oldFiles = [ + { file: this.file2, path: '/folder/file-name-2' }, + { file: this.file1, path: '/folder/subfolder/file-name-1' } + ] + const oldDocs = [ + { doc: this.doc2, path: '/folder/doc-name-2' }, + { doc: this.doc1, path: '/folder/subfolder/doc-name-1' } + ] + return this.DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, projectHistoryId, userId, { + oldFiles, + oldDocs, + newProject: this.newProject + }) + .should.equal(true) + }) + }) + }) + + return describe('_cleanUpDoc', function() { + beforeEach(function() { + this.doc = { + _id: ObjectId(), + name: 'test.tex' + } + this.path = '/path/to/doc' + this.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() + this.ProjectEntityMongoUpdateHandler._insertDeletedDocReference = sinon + .stub() + .yields() + this.DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() + this.DocstoreManager.deleteDoc = sinon.stub().yields() + return (this.callback = sinon.stub()) + }) + + describe('when the doc is the root doc', function() { + beforeEach(function() { + this.project.rootDoc_id = this.doc._id + return this.ProjectEntityUpdateHandler._cleanUpDoc( + this.project, + this.doc, + this.path, + userId, + this.callback + ) + }) + + it('should unset the root doc', function() { + return this.ProjectEntityUpdateHandler.unsetRootDoc + .calledWith(project_id) + .should.equal(true) + }) + + it('should delete the doc in the doc updater', function() { + return this.DocumentUpdaterHandler.deleteDoc.calledWith( + project_id, + this.doc._id.toString() + ) + }) + + it('should insert the doc into the deletedDocs array', function() { + return this.ProjectEntityMongoUpdateHandler._insertDeletedDocReference + .calledWith(this.project._id, this.doc) + .should.equal(true) + }) + + it('should delete the doc in the doc store', function() { + return this.DocstoreManager.deleteDoc + .calledWith(project_id, this.doc._id.toString()) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the doc is not the root doc', function() { + beforeEach(function() { + this.project.rootDoc_id = ObjectId() + return this.ProjectEntityUpdateHandler._cleanUpDoc( + this.project, + this.doc, + this.path, + userId, + this.callback + ) + }) + + it('should not unset the root doc', function() { + return this.ProjectEntityUpdateHandler.unsetRootDoc.called.should.equal( + false + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectGetterTests.js b/services/web/test/unit/src/Project/ProjectGetterTests.js new file mode 100644 index 0000000000..f194021a25 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectGetterTests.js @@ -0,0 +1,385 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Project/ProjectGetter.js' +const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongojs') +const { assert } = require('chai') + +describe('ProjectGetter', function() { + beforeEach(function() { + this.callback = sinon.stub() + return (this.ProjectGetter = SandboxedModule.require(modulePath, { + requires: { + '../../infrastructure/mongojs': { + db: (this.db = { + projects: {}, + users: {} + }), + ObjectId + }, + 'metrics-sharelatex': { + timeAsyncMethod: sinon.stub() + }, + '../../models/Project': { + Project: (this.Project = {}) + }, + '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../../infrastructure/LockManager': (this.LockManager = { + runWithLock: sinon.spy((namespace, id, runner, callback) => + runner(callback) + ) + }), + './ProjectEntityMongoUpdateHandler': { + lockKey(project_id) { + return project_id + } + }, + 'logger-sharelatex': { + err() {}, + log() {} + } + } + })) + }) + + describe('getProjectWithoutDocLines', function() { + beforeEach(function() { + this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') } + return (this.ProjectGetter.getProject = sinon.stub().yields()) + }) + + return describe('passing an id', function() { + beforeEach(function() { + return this.ProjectGetter.getProjectWithoutDocLines( + this.project_id, + this.callback + ) + }) + + it('should call find with the project id', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should exclude the doc lines', function() { + const excludes = { + 'rootFolder.docs.lines': 0, + 'rootFolder.folders.docs.lines': 0, + 'rootFolder.folders.folders.docs.lines': 0, + 'rootFolder.folders.folders.folders.docs.lines': 0, + 'rootFolder.folders.folders.folders.folders.docs.lines': 0, + 'rootFolder.folders.folders.folders.folders.folders.docs.lines': 0, + 'rootFolder.folders.folders.folders.folders.folders.folders.docs.lines': 0, + 'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines': 0 + } + + return this.ProjectGetter.getProject + .calledWith(this.project_id, excludes) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('getProjectWithOnlyFolders', function() { + beforeEach(function() { + this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') } + return (this.ProjectGetter.getProject = sinon.stub().yields()) + }) + + return describe('passing an id', function() { + beforeEach(function() { + return this.ProjectGetter.getProjectWithOnlyFolders( + this.project_id, + this.callback + ) + }) + + it('should call find with the project id', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should exclude the docs and files linesaaaa', function() { + const excludes = { + 'rootFolder.docs': 0, + 'rootFolder.fileRefs': 0, + 'rootFolder.folders.docs': 0, + 'rootFolder.folders.fileRefs': 0, + 'rootFolder.folders.folders.docs': 0, + 'rootFolder.folders.folders.fileRefs': 0, + 'rootFolder.folders.folders.folders.docs': 0, + 'rootFolder.folders.folders.folders.fileRefs': 0, + 'rootFolder.folders.folders.folders.folders.docs': 0, + 'rootFolder.folders.folders.folders.folders.fileRefs': 0, + 'rootFolder.folders.folders.folders.folders.folders.docs': 0, + 'rootFolder.folders.folders.folders.folders.folders.fileRefs': 0, + 'rootFolder.folders.folders.folders.folders.folders.folders.docs': 0, + 'rootFolder.folders.folders.folders.folders.folders.folders.fileRefs': 0, + 'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs': 0, + 'rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs': 0 + } + return this.ProjectGetter.getProject + .calledWith(this.project_id, excludes) + .should.equal(true) + }) + + return it('should call the callback with the project', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('getProject', function() { + beforeEach(function() { + this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') } + return (this.db.projects.find = sinon + .stub() + .callsArgWith(2, null, [this.project])) + }) + + describe('without projection', function() { + describe('with project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProject(this.project_id, this.callback) + }) + + return it('should call find with the project id', function() { + expect(this.db.projects.find.callCount).to.equal(1) + return expect(this.db.projects.find.lastCall.args[0]).to.deep.equal({ + _id: ObjectId(this.project_id) + }) + }) + }) + + return describe('without project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProject(null, this.callback) + }) + + return it('should callback with error', function() { + expect(this.db.projects.find.callCount).to.equal(0) + return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + + return describe('with projection', function() { + beforeEach(function() { + return (this.projection = { _id: 1 }) + }) + + describe('with project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProject( + this.project_id, + this.projection, + this.callback + ) + }) + + return it('should call find with the project id', function() { + expect(this.db.projects.find.callCount).to.equal(1) + expect(this.db.projects.find.lastCall.args[0]).to.deep.equal({ + _id: ObjectId(this.project_id) + }) + return expect(this.db.projects.find.lastCall.args[1]).to.deep.equal( + this.projection + ) + }) + }) + + return describe('without project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProject(null, this.callback) + }) + + return it('should callback with error', function() { + expect(this.db.projects.find.callCount).to.equal(0) + return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + }) + + describe('getProjectWithoutLock', function() { + beforeEach(function() { + this.project = { _id: (this.project_id = '56d46b0a1d3422b87c5ebcb1') } + return (this.db.projects.find = sinon + .stub() + .callsArgWith(2, null, [this.project])) + }) + + describe('without projection', function() { + describe('with project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProjectWithoutLock( + this.project_id, + this.callback + ) + }) + + return it('should call find with the project id', function() { + expect(this.db.projects.find.callCount).to.equal(1) + return expect(this.db.projects.find.lastCall.args[0]).to.deep.equal({ + _id: ObjectId(this.project_id) + }) + }) + }) + + return describe('without project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProjectWithoutLock(null, this.callback) + }) + + return it('should callback with error', function() { + expect(this.db.projects.find.callCount).to.equal(0) + return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + + return describe('with projection', function() { + beforeEach(function() { + return (this.projection = { _id: 1 }) + }) + + describe('with project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProjectWithoutLock( + this.project_id, + this.projection, + this.callback + ) + }) + + return it('should call find with the project id', function() { + expect(this.db.projects.find.callCount).to.equal(1) + expect(this.db.projects.find.lastCall.args[0]).to.deep.equal({ + _id: ObjectId(this.project_id) + }) + return expect(this.db.projects.find.lastCall.args[1]).to.deep.equal( + this.projection + ) + }) + }) + + return describe('without project id', function() { + beforeEach(function() { + return this.ProjectGetter.getProjectWithoutLock(null, this.callback) + }) + + return it('should callback with error', function() { + expect(this.db.projects.find.callCount).to.equal(0) + return expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error) + }) + }) + }) + }) + + describe('findAllUsersProjects', function() { + beforeEach(function() { + this.fields = { mock: 'fields' } + this.Project.find = sinon.stub() + this.Project.find + .withArgs({ owner_ref: this.user_id }, this.fields) + .yields(null, ['mock-owned-projects']) + this.CollaboratorsHandler.getProjectsUserIsMemberOf = sinon.stub() + this.CollaboratorsHandler.getProjectsUserIsMemberOf + .withArgs(this.user_id, this.fields) + .yields(null, { + readAndWrite: ['mock-rw-projects'], + readOnly: ['mock-ro-projects'], + tokenReadAndWrite: ['mock-token-rw-projects'], + tokenReadOnly: ['mock-token-ro-projects'] + }) + return this.ProjectGetter.findAllUsersProjects( + this.user_id, + this.fields, + this.callback + ) + }) + + return it('should call the callback with all the projects', function() { + return this.callback + .calledWith(null, { + owned: ['mock-owned-projects'], + readAndWrite: ['mock-rw-projects'], + readOnly: ['mock-ro-projects'], + tokenReadAndWrite: ['mock-token-rw-projects'], + tokenReadOnly: ['mock-token-ro-projects'] + }) + .should.equal(true) + }) + }) + + return describe('getProjectIdByReadAndWriteToken', function() { + describe('when project find returns project', function() { + this.beforeEach(function() { + this.Project.findOne = sinon.stub().yields(null, { _id: 'project-id' }) + return this.ProjectGetter.getProjectIdByReadAndWriteToken( + 'token', + this.callback + ) + }) + + it('should find project with token', function() { + return this.Project.findOne + .calledWithMatch({ 'tokens.readAndWrite': 'token' }) + .should.equal(true) + }) + + return it('should callback with project id', function() { + return this.callback.calledWith(null, 'project-id').should.equal(true) + }) + }) + + describe('when project not found', function() { + this.beforeEach(function() { + this.Project.findOne = sinon.stub().yields() + return this.ProjectGetter.getProjectIdByReadAndWriteToken( + 'token', + this.callback + ) + }) + + return it('should callback empty', function() { + return expect(this.callback.firstCall.args.length).to.equal(0) + }) + }) + + return describe('when project find returns error', function() { + this.beforeEach(function() { + this.Project.findOne = sinon.stub().yields('error') + return this.ProjectGetter.getProjectIdByReadAndWriteToken( + 'token', + this.callback + ) + }) + + return it('should callback with error', function() { + return this.callback.calledWith('error').should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectHelperTests.js b/services/web/test/unit/src/Project/ProjectHelperTests.js new file mode 100644 index 0000000000..d68706e54a --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectHelperTests.js @@ -0,0 +1,53 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/Project/ProjectHelper.js' +const SandboxedModule = require('sandboxed-module') + +describe('ProjectHelper', function() { + beforeEach(function() { + return (this.ProjectHelper = SandboxedModule.require(modulePath)) + }) + + return describe('compilerFromV1Engine', function() { + it('returns the correct engine for latex_dvipdf', function() { + return expect( + this.ProjectHelper.compilerFromV1Engine('latex_dvipdf') + ).to.equal('latex') + }) + + it('returns the correct engine for pdflatex', function() { + return expect( + this.ProjectHelper.compilerFromV1Engine('pdflatex') + ).to.equal('pdflatex') + }) + + it('returns the correct engine for xelatex', function() { + return expect( + this.ProjectHelper.compilerFromV1Engine('xelatex') + ).to.equal('xelatex') + }) + + return it('returns the correct engine for lualatex', function() { + return expect( + this.ProjectHelper.compilerFromV1Engine('lualatex') + ).to.equal('lualatex') + }) + }) +}) + +// describe "ensureNameIsUnique", -> +// see tests for: ProjectDetailsHandler.ensureProjectNameIsUnique, which calls here. diff --git a/services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js b/services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js new file mode 100644 index 0000000000..2dc6db9e14 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectHistoryHandlerTests.js @@ -0,0 +1,170 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const { assert } = require('chai') +const should = chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Project/ProjectHistoryHandler' +const SandboxedModule = require('sandboxed-module') +const { ObjectId } = require('mongoose').Types + +describe('ProjectHistoryHandler', function() { + const project_id = '4eecb1c1bffa66588e0000a1' + const userId = 1234 + + beforeEach(function() { + let Project + this.ProjectModel = Project = (function() { + Project = class Project { + static initClass() { + this.prototype.rootFolder = [this.rootFolder] + } + constructor(options) { + this._id = project_id + this.name = 'project_name_here' + this.rev = 0 + } + } + Project.initClass() + return Project + })() + this.project = new this.ProjectModel() + + this.callback = sinon.stub() + + return (this.ProjectHistoryHandler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }), + 'settings-sharelatex': (this.Settings = {}), + '../../models/Project': { + Project: this.ProjectModel + }, + './ProjectDetailsHandler': (this.ProjectDetailsHandler = {}), + '../History/HistoryManager': (this.HistoryManager = {}), + './ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}) + } + })) + }) + + return describe('starting history for an existing project', function() { + beforeEach(function() { + this.newHistoryId = 123456789 + this.HistoryManager.initializeProject = sinon + .stub() + .callsArgWith(0, null, { overleaf_id: this.newHistoryId }) + this.HistoryManager.flushProject = sinon.stub().callsArg(1) + return (this.ProjectEntityUpdateHandler.resyncProjectHistory = sinon + .stub() + .callsArg(1)) + }) + + describe('when the history does not already exist', function() { + beforeEach(function() { + this.ProjectDetailsHandler.getDetails = sinon + .stub() + .withArgs(project_id) + .callsArgWith(1, null, this.project) + this.ProjectModel.update = sinon.stub().callsArgWith(2, null, { n: 1 }) + return this.ProjectHistoryHandler.ensureHistoryExistsForProject( + project_id, + this.callback + ) + }) + + it('should get any existing history id for the project', function() { + return this.ProjectDetailsHandler.getDetails + .calledWith(project_id) + .should.equal(true) + }) + + it('should initialize a new history in the v1 history service', function() { + return this.HistoryManager.initializeProject.called.should.equal.true + }) + + it('should set the new history id on the project', function() { + return this.ProjectModel.update + .calledWith( + { _id: project_id, 'overleaf.history.id': { $exists: false } }, + { 'overleaf.history.id': this.newHistoryId } + ) + .should.equal(true) + }) + + it('should resync the project history', function() { + return this.ProjectEntityUpdateHandler.resyncProjectHistory + .calledWith(project_id) + .should.equal(true) + }) + + it('should flush the project history', function() { + return this.HistoryManager.flushProject + .calledWith(project_id) + .should.equal(true) + }) + + return it('should call the callback without an error', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the history already exists', function() { + beforeEach(function() { + this.project.overleaf = { history: { id: 1234 } } + this.ProjectDetailsHandler.getDetails = sinon + .stub() + .withArgs(project_id) + .callsArgWith(1, null, this.project) + this.ProjectModel.update = sinon.stub() + return this.ProjectHistoryHandler.ensureHistoryExistsForProject( + project_id, + this.callback + ) + }) + + it('should get any existing history id for the project', function() { + return this.ProjectDetailsHandler.getDetails + .calledWith(project_id) + .should.equal(true) + }) + + it('should not initialize a new history in the v1 history service', function() { + return this.HistoryManager.initializeProject.called.should.equal(false) + }) + + it('should not set the new history id on the project', function() { + return this.ProjectModel.update.called.should.equal(false) + }) + + it('should not resync the project history', function() { + return this.ProjectEntityUpdateHandler.resyncProjectHistory.called.should.equal( + false + ) + }) + + it('should not flush the project history', function() { + return this.HistoryManager.flushProject.called.should.equal(false) + }) + + return it('should call the callback', function() { + return this.callback.calledWith().should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectLocatorTests.js b/services/web/test/unit/src/Project/ProjectLocatorTests.js new file mode 100644 index 0000000000..542114f477 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectLocatorTests.js @@ -0,0 +1,604 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-use-before-define, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const spies = require('chai-spies') +const chai = require('chai').use(spies) +const { assert } = require('chai') +const should = chai.should() +const modulePath = '../../../../app/src/Features/Project/ProjectLocator' +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const Errors = require('../../../../app/src/Features/Errors/Errors') +const { expect } = require('chai') +var Project = (Project = class Project {}) + +const project = { _id: '1234566', rootFolder: [] } +const rootDoc = { name: 'rootDoc', _id: 'das239djd' } +const doc1 = { name: 'otherDoc.txt', _id: 'dsad2ddd' } +const doc2 = { name: 'docname.txt', _id: 'dsad2ddddd' } +const file1 = { name: 'file1', _id: 'dsa9lkdsad' } +const subSubFile = { name: 'subSubFile', _id: 'd1d2dk' } +const subSubDoc = { name: 'subdoc.txt', _id: '321dmdwi' } +const secondSubFolder = { + name: 'secondSubFolder', + _id: 'dsa3e23', + docs: [subSubDoc], + fileRefs: [subSubFile], + folders: [] +} +const subFolder = { + name: 'subFolder', + _id: 'dsadsa93', + folders: [secondSubFolder, null], + docs: [], + fileRefs: [] +} +const subFolder1 = { name: 'subFolder1', _id: '123asdjoij' } + +const rootFolder = { + _id: '123sdskd', + docs: [doc1, doc2, null, rootDoc], + fileRefs: [file1], + folders: [subFolder1, subFolder] +} + +project.rootFolder[0] = rootFolder +project.rootDoc_id = rootDoc._id + +describe('ProjectLocator', function() { + beforeEach(function() { + Project.findById = (project_id, callback) => { + return callback(null, project) + } + this.ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, project) + } + return (this.locator = SandboxedModule.require(modulePath, { + requires: { + '../../models/Project': { Project }, + '../../models/User': { User: this.User }, + './ProjectGetter': this.ProjectGetter, + 'logger-sharelatex': { + log() {}, + err() {}, + warn() {} + } + } + })) + }) + + describe('finding a doc', function() { + it('finds one at the root level', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: doc2._id, type: 'docs' }, + function(err, foundElement, path, parentFolder) { + assert(err == null) + foundElement._id.should.equal(doc2._id) + path.fileSystem.should.equal(`/${doc2.name}`) + parentFolder._id.should.equal(project.rootFolder[0]._id) + path.mongo.should.equal('rootFolder.0.docs.1') + return done() + } + ) + }) + + it('when it is nested', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: subSubDoc._id, type: 'doc' }, + function(err, foundElement, path, parentFolder) { + assert(err == null) + should.equal(foundElement._id, subSubDoc._id) + path.fileSystem.should.equal( + `/${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}` + ) + parentFolder._id.should.equal(secondSubFolder._id) + path.mongo.should.equal('rootFolder.0.folders.1.folders.0.docs.0') + return done() + } + ) + }) + + return it('should give error if element could not be found', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: 'ddsd432nj42', type: 'docs' }, + function(err, foundElement, path, parentFolder) { + err.should.deep.equal(new Errors.NotFoundError('entity not found')) + return done() + } + ) + }) + }) + + describe('finding a folder', function() { + it('should return root folder when looking for root folder', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: rootFolder._id, type: 'folder' }, + function(err, foundElement, path, parentFolder) { + assert(!err) + foundElement._id.should.equal(rootFolder._id) + return done() + } + ) + }) + + it('when at root', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: subFolder._id, type: 'folder' }, + function(err, foundElement, path, parentFolder) { + assert(!err) + foundElement._id.should.equal(subFolder._id) + path.fileSystem.should.equal(`/${subFolder.name}`) + parentFolder._id.should.equal(rootFolder._id) + path.mongo.should.equal('rootFolder.0.folders.1') + return done() + } + ) + }) + + return it('when deeply nested', function(done) { + return this.locator.findElement( + { + project_id: project._id, + element_id: secondSubFolder._id, + type: 'folder' + }, + function(err, foundElement, path, parentFolder) { + assert(!err) + foundElement._id.should.equal(secondSubFolder._id) + path.fileSystem.should.equal( + `/${subFolder.name}/${secondSubFolder.name}` + ) + parentFolder._id.should.equal(subFolder._id) + path.mongo.should.equal('rootFolder.0.folders.1.folders.0') + return done() + } + ) + }) + }) + + describe('finding a file', function() { + it('when at root', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: file1._id, type: 'fileRefs' }, + function(err, foundElement, path, parentFolder) { + assert(!err) + foundElement._id.should.equal(file1._id) + path.fileSystem.should.equal(`/${file1.name}`) + parentFolder._id.should.equal(rootFolder._id) + path.mongo.should.equal('rootFolder.0.fileRefs.0') + return done() + } + ) + }) + + return it('when deeply nested', function(done) { + return this.locator.findElement( + { + project_id: project._id, + element_id: subSubFile._id, + type: 'fileRefs' + }, + function(err, foundElement, path, parentFolder) { + assert(!err) + foundElement._id.should.equal(subSubFile._id) + path.fileSystem.should.equal( + `/${subFolder.name}/${secondSubFolder.name}/${subSubFile.name}` + ) + parentFolder._id.should.equal(secondSubFolder._id) + path.mongo.should.equal('rootFolder.0.folders.1.folders.0.fileRefs.0') + return done() + } + ) + }) + }) + + describe('finding an element with wrong element type', function() { + it('should add an s onto the element type', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: subSubDoc._id, type: 'doc' }, + function(err, foundElement, path, parentFolder) { + assert(!err) + foundElement._id.should.equal(subSubDoc._id) + return done() + } + ) + }) + + return it('should convert file to fileRefs', function(done) { + return this.locator.findElement( + { project_id: project._id, element_id: file1._id, type: 'fileRefs' }, + function(err, foundElement, path, parentFolder) { + assert(!err) + foundElement._id.should.equal(file1._id) + return done() + } + ) + }) + }) + + describe('should be able to take actual project as well as id', function() { + const doc3 = { + _id: '123dsdj3', + name: 'doc3' + } + const rootFolder2 = { + _id: '123sddedskd', + docs: [doc3] + } + const project2 = { + _id: '1234566', + rootFolder: [rootFolder2] + } + return it('should find doc in project', function(done) { + return this.locator.findElement( + { project: project2, element_id: doc3._id, type: 'docs' }, + function(err, foundElement, path, parentFolder) { + assert(err == null) + foundElement._id.should.equal(doc3._id) + path.fileSystem.should.equal(`/${doc3.name}`) + parentFolder._id.should.equal(project2.rootFolder[0]._id) + path.mongo.should.equal('rootFolder.0.docs.0') + return done() + } + ) + }) + }) + + describe('finding root doc', function() { + it('should return root doc when passed project', function(done) { + return this.locator.findRootDoc(project, function(err, doc) { + assert(err == null) + doc._id.should.equal(rootDoc._id) + return done() + }) + }) + + it('should return root doc when passed project_id', function(done) { + return this.locator.findRootDoc(project._id, function(err, doc) { + assert(err == null) + doc._id.should.equal(rootDoc._id) + return done() + }) + }) + + it('should return null when the project has no rootDoc', function(done) { + project.rootDoc_id = null + return this.locator.findRootDoc(project, function(err, doc) { + assert(err == null) + expect(doc).to.equal(null) + return done() + }) + }) + + return it('should return null when the rootDoc_id no longer exists', function(done) { + project.rootDoc_id = 'doesntexist' + return this.locator.findRootDoc(project, function(err, doc) { + assert(err == null) + expect(doc).to.equal(null) + return done() + }) + }) + }) + + describe('findElementByPath', function() { + it('should take a doc path and return the element for a root level document', function(done) { + const path = `${doc1.name}` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(doc1) + expect(type).to.equal('doc') + return done() + }) + }) + + it('should take a doc path and return the element for a root level document with a starting slash', function(done) { + const path = `/${doc1.name}` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(doc1) + expect(type).to.equal('doc') + return done() + }) + }) + + it('should take a doc path and return the element for a nested document', function(done) { + const path = `${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(subSubDoc) + expect(type).to.equal('doc') + return done() + }) + }) + + it('should take a file path and return the element for a root level document', function(done) { + const path = `${file1.name}` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(file1) + expect(type).to.equal('file') + return done() + }) + }) + + it('should take a file path and return the element for a nested document', function(done) { + const path = `${subFolder.name}/${secondSubFolder.name}/${ + subSubFile.name + }` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(subSubFile) + expect(type).to.equal('file') + return done() + }) + }) + + it('should take a file path and return the element for a nested document case insenstive', function(done) { + const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(subSubFile) + expect(type).to.equal('file') + return done() + }) + }) + + it('should take a file path and return the element for a nested folder', function(done) { + const path = `${subFolder.name}/${secondSubFolder.name}` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(secondSubFolder) + expect(type).to.equal('folder') + return done() + }) + }) + + it('should take a file path and return the root folder', function(done) { + const path = '/' + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + element.should.deep.equal(rootFolder) + expect(type).to.equal('folder') + return done() + }) + }) + + it('should return an error if the file can not be found inside know folder', function(done) { + const path = `${subFolder.name}/${secondSubFolder.name}/exist.txt` + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + err.should.not.equal(undefined) + assert.equal(element, undefined) + expect(type).to.be.undefined + return done() + }) + }) + + it('should return an error if the file can not be found inside unknown folder', function(done) { + const path = 'this/does/not/exist.txt' + return this.locator.findElementByPath({ project, path }, function( + err, + element, + type + ) { + err.should.not.equal(undefined) + assert.equal(element, undefined) + expect(type).to.be.undefined + return done() + }) + }) + + describe('where duplicate folder exists', function() { + beforeEach(function() { + this.duplicateFolder = { + name: 'duplicate1', + _id: '1234', + folders: [ + { + name: '1', + docs: [{ name: 'main.tex', _id: '456' }], + folders: [], + fileRefs: [] + } + ], + docs: [(this.doc = { name: 'main.tex', _id: '456' })], + fileRefs: [] + } + return (this.project = { + rootFolder: [ + { + folders: [this.duplicateFolder, this.duplicateFolder], + fileRefs: [], + docs: [] + } + ] + }) + }) + + it('should not call the callback more than once', function(done) { + const path = `${this.duplicateFolder.name}/${this.doc.name}` + return this.locator.findElementByPath( + { project: this.project, path }, + () => done() + ) + }) // mocha will throw exception if done called multiple times + + return it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', function(done) { + const path = `${this.duplicateFolder.name}/1/main.tex` + return this.locator.findElementByPath( + { project: this.project, path }, + () => done() + ) + }) + }) // mocha will throw exception if done called multiple times + + describe('with a null doc', function() { + beforeEach(function() { + return (this.project = { + rootFolder: [ + { + folders: [], + fileRefs: [], + docs: [{ name: 'main.tex' }, null, { name: 'other.tex' }] + } + ] + }) + }) + + return it('should not crash with a null', function(done) { + const path = '/other.tex' + return this.locator.findElementByPath( + { project: this.project, path }, + function(err, element) { + element.name.should.equal('other.tex') + return done() + } + ) + }) + }) + + describe('with a null project', function() { + beforeEach(function() { + return (this.ProjectGetter = { getProject: sinon.stub().callsArg(2) }) + }) + + return it('should not crash with a null', function(done) { + const path = '/other.tex' + return this.locator.findElementByPath( + { project_id: project._id, path }, + function(err, element) { + expect(err).to.exist + return done() + } + ) + }) + }) + + return describe('with a project_id', () => + it('should take a doc path and return the element for a root level document', function(done) { + const path = `${doc1.name}` + return this.locator.findElementByPath( + { project_id: project._id, path }, + (err, element, type) => { + this.ProjectGetter.getProject + .calledWith(project._id, { rootFolder: true, rootDoc_id: true }) + .should.equal(true) + element.should.deep.equal(doc1) + expect(type).to.equal('doc') + return done() + } + ) + })) + }) + + return describe('findUsersProjectByName finding a project by user_id and project name', function() { + it('should return the project from an array case insenstive', function(done) { + const user_id = '123jojoidns' + const stubbedProject = { name: 'findThis' } + const projects = { + owned: [ + { name: 'notThis' }, + { name: 'wellll' }, + stubbedProject, + { name: 'Noooo' } + ] + } + this.ProjectGetter.findAllUsersProjects = sinon + .stub() + .callsArgWith(2, null, projects) + return this.locator.findUsersProjectByName( + user_id, + stubbedProject.name.toLowerCase(), + function(err, project) { + project.should.equal(stubbedProject) + return done() + } + ) + }) + + it('should return the project which is not archived', function(done) { + const user_id = '123jojoidns' + const stubbedProject = { name: 'findThis', _id: 12331321 } + const projects = { + owned: [ + { name: 'notThis' }, + { name: 'wellll' }, + { name: 'findThis', archived: true }, + stubbedProject, + { name: 'findThis', archived: true }, + { name: 'Noooo' } + ] + } + this.ProjectGetter.findAllUsersProjects = sinon + .stub() + .callsArgWith(2, null, projects) + return this.locator.findUsersProjectByName( + user_id, + stubbedProject.name.toLowerCase(), + function(err, project) { + project._id.should.equal(stubbedProject._id) + return done() + } + ) + }) + + return it('should search collab projects as well', function(done) { + const user_id = '123jojoidns' + const stubbedProject = { name: 'findThis' } + const projects = { + owned: [{ name: 'notThis' }, { name: 'wellll' }, { name: 'Noooo' }], + readAndWrite: [stubbedProject] + } + this.ProjectGetter.findAllUsersProjects = sinon + .stub() + .callsArgWith(2, null, projects) + return this.locator.findUsersProjectByName( + user_id, + stubbedProject.name.toLowerCase(), + function(err, project) { + project.should.equal(stubbedProject) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectOptionsHandlerTests.js b/services/web/test/unit/src/Project/ProjectOptionsHandlerTests.js new file mode 100644 index 0000000000..667f36b06b --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectOptionsHandlerTests.js @@ -0,0 +1,161 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, + no-useless-constructor, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const { expect } = chai +const should = chai.should() +const modulePath = + '../../../../app/src/Features/Project/ProjectOptionsHandler.js' +const SandboxedModule = require('sandboxed-module') + +describe('ProjectOptionsHandler', function() { + const project_id = '4eecaffcbffa66588e000008' + + beforeEach(function() { + let Project + this.projectModel = Project = class Project { + constructor(options) {} + } + this.projectModel.update = sinon.spy() + + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + '../../models/Project': { Project: this.projectModel }, + 'settings-sharelatex': { + languages: [ + { name: 'English', code: 'en' }, + { name: 'French', code: 'fr' } + ], + imageRoot: 'docker-repo/subdir', + allowedImageNames: [ + { imageName: 'texlive-0000.0', imageDesc: 'test image 0' }, + { imageName: 'texlive-1234.5', imageDesc: 'test image 1' } + ] + }, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + describe('Setting the compiler', function() { + it('should perform and update on mongo', function(done) { + this.handler.setCompiler(project_id, 'xeLaTeX', err => { + const args = this.projectModel.update.args[0] + args[0]._id.should.equal(project_id) + args[1].compiler.should.equal('xelatex') + return done() + }) + return this.projectModel.update.args[0][3]() + }) + + return it('should not perform and update on mongo if it is not a reconised compiler', function(done) { + return this.handler.setCompiler(project_id, 'something', err => { + this.projectModel.update.called.should.equal(false) + return done() + }) + }) + }) + + describe('Setting the imageName', function() { + it('should perform and update on mongo', function(done) { + this.handler.setImageName(project_id, 'texlive-1234.5', err => { + const args = this.projectModel.update.args[0] + args[0]._id.should.equal(project_id) + args[1].imageName.should.equal('docker-repo/subdir/texlive-1234.5') + return done() + }) + return this.projectModel.update.args[0][3]() + }) + + return it('should not perform and update on mongo if it is not a reconised compiler', function(done) { + return this.handler.setImageName(project_id, 'something', err => { + this.projectModel.update.called.should.equal(false) + return done() + }) + }) + }) + + describe('setting the spellCheckLanguage', function() { + it('should perform and update on mongo', function(done) { + this.handler.setSpellCheckLanguage(project_id, 'fr', err => { + const args = this.projectModel.update.args[0] + args[0]._id.should.equal(project_id) + args[1].spellCheckLanguage.should.equal('fr') + return done() + }) + return this.projectModel.update.args[0][3]() + }) + + it('should not perform and update on mongo if it is not a reconised compiler', function(done) { + return this.handler.setSpellCheckLanguage( + project_id, + 'no a lang', + err => { + this.projectModel.update.called.should.equal(false) + return done() + } + ) + }) + + return it('should perform and update on mongo if the language is blank (means turn it off)', function(done) { + this.handler.setSpellCheckLanguage(project_id, '', err => { + this.projectModel.update.called.should.equal(true) + return done() + }) + return this.projectModel.update.args[0][3]() + }) + }) + + describe('setting the brandVariationId', function() { + it('should perform and update on mongo', function(done) { + this.handler.setBrandVariationId(project_id, '123', err => { + const args = this.projectModel.update.args[0] + args[0]._id.should.equal(project_id) + args[1].brandVariationId.should.equal('123') + return done() + }) + return this.projectModel.update.args[0][3]() + }) + + it('should not perform and update on mongo if there is no brand variation', function(done) { + return this.handler.setBrandVariationId(project_id, null, err => { + this.projectModel.update.called.should.equal(false) + return done() + }) + }) + + return it('should not perform and update on mongo if brand variation is an empty string', function(done) { + return this.handler.setBrandVariationId(project_id, '', err => { + this.projectModel.update.called.should.equal(false) + return done() + }) + }) + }) + + return describe('unsetting the brandVariationId', () => + it('should perform and update on mongo', function(done) { + this.handler.unsetBrandVariationId(project_id, err => { + const args = this.projectModel.update.args[0] + args[0]._id.should.equal(project_id) + expect(args[1]).to.deep.equal({ $unset: { brandVariationId: 1 } }) + return done() + }) + return this.projectModel.update.args[0][3]() + })) +}) diff --git a/services/web/test/unit/src/Project/ProjectRootDocManagerTests.js b/services/web/test/unit/src/Project/ProjectRootDocManagerTests.js new file mode 100644 index 0000000000..e6577fcd19 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectRootDocManagerTests.js @@ -0,0 +1,689 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = + '../../../../app/src/Features/Project/ProjectRootDocManager.js' +const SandboxedModule = require('sandboxed-module') + +describe('ProjectRootDocManager', function() { + beforeEach(function() { + this.project_id = 'project-123' + this.docPaths = { + 'doc-id-1': '/chapter1.tex', + 'doc-id-2': '/main.tex', + 'doc-id-3': '/nested/chapter1a.tex', + 'doc-id-4': '/nested/chapter1b.tex' + } + this.sl_req_id = 'sl-req-id-123' + this.callback = sinon.stub() + this.globbyFiles = ['a.tex', 'b.tex', 'main.tex'] + this.globby = sinon.stub().returns( + new Promise(resolve => { + return resolve(this.globbyFiles) + }) + ) + this.fs = { + readFile: sinon.stub().callsArgWith(2, new Error('file not found')), + stat: sinon.stub().callsArgWith(1, null, { size: 100 }) + } + return (this.ProjectRootDocManager = SandboxedModule.require(modulePath, { + requires: { + './ProjectEntityHandler': (this.ProjectEntityHandler = {}), + './ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}), + './ProjectGetter': (this.ProjectGetter = {}), + globby: this.globby, + fs: this.fs + } + })) + }) + + describe('setRootDocAutomatically', function() { + describe('when there is a suitable root doc', function() { + beforeEach(function(done) { + this.docs = { + '/chapter1.tex': { + _id: 'doc-id-1', + lines: [ + 'something else', + '\\begin{document}', + 'Hello world', + '\\end{document}' + ] + }, + '/main.tex': { + _id: 'doc-id-2', + lines: [ + 'different line', + '\\documentclass{article}', + '\\input{chapter1}' + ] + }, + '/nested/chapter1a.tex': { + _id: 'doc-id-3', + lines: ['Hello world'] + }, + '/nested/chapter1b.tex': { + _id: 'doc-id-4', + lines: ['Hello world'] + } + } + + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocAutomatically( + this.project_id, + done + ) + }) + + it('should check the docs of the project', function() { + return this.ProjectEntityHandler.getAllDocs + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should set the root doc to the doc containing a documentclass', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, 'doc-id-2') + .should.equal(true) + }) + }) + + describe('when the root doc is an Rtex file', function() { + beforeEach(function() { + this.docs = { + '/chapter1.tex': { + _id: 'doc-id-1', + lines: ['\\begin{document}', 'Hello world', '\\end{document}'] + }, + '/main.Rtex': { + _id: 'doc-id-2', + lines: ['\\documentclass{article}', '\\input{chapter1}'] + } + } + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocAutomatically( + this.project_id, + this.callback + ) + }) + + return it('should set the root doc to the doc containing a documentclass', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, 'doc-id-2') + .should.equal(true) + }) + }) + + return describe('when there is no suitable root doc', function() { + beforeEach(function(done) { + this.docs = { + '/chapter1.tex': { + _id: 'doc-id-1', + lines: ['\\begin{document}', 'Hello world', '\\end{document}'] + }, + '/style.bst': { + _id: 'doc-id-2', + lines: ['%Example: \\documentclass{article}'] + } + } + this.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, this.docs) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocAutomatically( + this.project_id, + done + ) + }) + + return it('should not set the root doc to the doc containing a documentclass', function() { + return this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal( + false + ) + }) + }) + }) + + describe('findRootDocFileFromDirectory', function() { + beforeEach(function() { + this.fs.readFile + .withArgs('/foo/a.tex') + .callsArgWith(2, null, 'Hello World!') + this.fs.readFile + .withArgs('/foo/b.tex') + .callsArgWith(2, null, "I'm a little teapot, get me out of here.") + this.fs.readFile + .withArgs('/foo/main.tex') + .callsArgWith(2, null, "Help, I'm trapped in a unit testing factory") + this.fs.readFile + .withArgs('/foo/c.tex') + .callsArgWith(2, null, 'Tomato, tomahto.') + this.fs.readFile + .withArgs('/foo/a/a.tex') + .callsArgWith(2, null, 'Potato? Potahto. Potootee!') + return (this.documentclassContent = '% test\n\\documentclass\n% test') + }) + + describe('when there is a file in a subfolder', function() { + beforeEach(function() { + // have to splice globbyFiles weirdly because of the way the stubbed globby method handles references + return this.globbyFiles.splice( + 0, + this.globbyFiles.length, + 'c.tex', + 'a.tex', + 'a/a.tex', + 'b.tex' + ) + }) + + it('processes the root folder files first, and then the subfolder, in alphabetical order', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + (error, path) => { + expect(error).not.to.exist + expect(path).not.to.exist + sinon.assert.callOrder( + this.fs.readFile.withArgs('/foo/a.tex'), + this.fs.readFile.withArgs('/foo/b.tex'), + this.fs.readFile.withArgs('/foo/c.tex'), + this.fs.readFile.withArgs('/foo/a/a.tex') + ) + return done() + } + ) + }) + + return it('processes smaller files first', function(done) { + this.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 }) + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + (error, path) => { + expect(error).not.to.exist + expect(path).not.to.exist + sinon.assert.callOrder( + this.fs.readFile.withArgs('/foo/c.tex'), + this.fs.readFile.withArgs('/foo/a.tex'), + this.fs.readFile.withArgs('/foo/b.tex'), + this.fs.readFile.withArgs('/foo/a/a.tex') + ) + return done() + } + ) + }) + }) + + describe('when main.tex contains a documentclass', function() { + beforeEach(function() { + return this.fs.readFile + .withArgs('/foo/main.tex') + .callsArgWith(2, null, this.documentclassContent) + }) + + it('returns main.tex', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + (error, path, content) => { + expect(error).not.to.exist + expect(path).to.equal('main.tex') + expect(content).to.equal(this.documentclassContent) + return done() + } + ) + }) + + return it('processes main.text first and stops processing when it finds the content', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + () => { + expect(this.fs.readFile).to.be.calledWith('/foo/main.tex') + expect(this.fs.readFile).not.to.be.calledWith('/foo/a.tex') + return done() + } + ) + }) + }) + + describe('when a.tex contains a documentclass', function() { + beforeEach(function() { + return this.fs.readFile + .withArgs('/foo/a.tex') + .callsArgWith(2, null, this.documentclassContent) + }) + + it('returns a.tex', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + (error, path, content) => { + expect(error).not.to.exist + expect(path).to.equal('a.tex') + expect(content).to.equal(this.documentclassContent) + return done() + } + ) + }) + + return it('processes main.text first and stops processing when it finds the content', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + () => { + expect(this.fs.readFile).to.be.calledWith('/foo/main.tex') + expect(this.fs.readFile).to.be.calledWith('/foo/a.tex') + expect(this.fs.readFile).not.to.be.calledWith('/foo/b.tex') + return done() + } + ) + }) + }) + + describe('when there is no documentclass', function() { + it('returns null with no error', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + (error, path, content) => { + expect(error).not.to.exist + expect(path).not.to.exist + expect(content).not.to.exist + return done() + } + ) + }) + + return it('processes all the files', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + () => { + expect(this.fs.readFile).to.be.calledWith('/foo/main.tex') + expect(this.fs.readFile).to.be.calledWith('/foo/a.tex') + expect(this.fs.readFile).to.be.calledWith('/foo/b.tex') + return done() + } + ) + }) + }) + + return describe('when there is an error reading a file', function() { + beforeEach(function() { + return this.fs.readFile + .withArgs('/foo/a.tex') + .callsArgWith(2, new Error('something went wrong')) + }) + + return it('returns an error', function(done) { + return this.ProjectRootDocManager.findRootDocFileFromDirectory( + '/foo', + (error, path, content) => { + expect(error).to.exist + expect(path).not.to.exist + expect(content).not.to.exist + return done() + } + ) + }) + }) + }) + + describe('setRootDocFromName', function() { + describe('when there is a suitable root doc', function() { + beforeEach(function(done) { + this.docPaths = { + 'doc-id-1': '/chapter1.tex', + 'doc-id-2': '/main.tex', + 'doc-id-3': '/nested/chapter1a.tex', + 'doc-id-4': '/nested/chapter1b.tex' + } + this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + .stub() + .callsArgWith(1, null, this.docPaths) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocFromName( + this.project_id, + '/main.tex', + done + ) + }) + + it('should check the docs of the project', function() { + return this.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should set the root doc to main.tex', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, 'doc-id-2') + .should.equal(true) + }) + }) + + describe('when there is a suitable root doc but the leading slash is missing', function() { + beforeEach(function(done) { + this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + .stub() + .callsArgWith(1, null, this.docPaths) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocFromName( + this.project_id, + 'main.tex', + done + ) + }) + + it('should check the docs of the project', function() { + return this.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should set the root doc to main.tex', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, 'doc-id-2') + .should.equal(true) + }) + }) + + describe('when there is a suitable root doc with a basename match', function() { + beforeEach(function(done) { + this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + .stub() + .callsArgWith(1, null, this.docPaths) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocFromName( + this.project_id, + 'chapter1a.tex', + done + ) + }) + + it('should check the docs of the project', function() { + return this.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should set the root doc using the basename', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, 'doc-id-3') + .should.equal(true) + }) + }) + + describe('when there is a suitable root doc but the filename is in quotes', function() { + beforeEach(function(done) { + this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + .stub() + .callsArgWith(1, null, this.docPaths) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocFromName( + this.project_id, + "'main.tex'", + done + ) + }) + + it('should check the docs of the project', function() { + return this.ProjectEntityHandler.getAllDocPathsFromProjectById + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should set the root doc to main.tex', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, 'doc-id-2') + .should.equal(true) + }) + }) + + return describe('when there is no suitable root doc', function() { + beforeEach(function(done) { + this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + .stub() + .callsArgWith(1, null, this.docPaths) + this.ProjectEntityUpdateHandler.setRootDoc = sinon + .stub() + .callsArgWith(2) + return this.ProjectRootDocManager.setRootDocFromName( + this.project_id, + 'other.tex', + done + ) + }) + + return it('should not set the root doc', function() { + return this.ProjectEntityUpdateHandler.setRootDoc.called.should.equal( + false + ) + }) + }) + }) + + describe('ensureRootDocumentIsSet', function() { + beforeEach(function() { + this.project = {} + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project) + return (this.ProjectRootDocManager.setRootDocAutomatically = sinon + .stub() + .callsArgWith(1, null)) + }) + + describe('when the root doc is set', function() { + beforeEach(function() { + this.project.rootDoc_id = 'root-doc-id' + return this.ProjectRootDocManager.ensureRootDocumentIsSet( + this.project_id, + this.callback + ) + }) + + it('should find the project fetching only the rootDoc_id field', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { rootDoc_id: 1 }) + .should.equal(true) + }) + + it('should not try to update the project rootDoc_id', function() { + return this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + false + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when the root doc is not set', function() { + beforeEach(function() { + return this.ProjectRootDocManager.ensureRootDocumentIsSet( + this.project_id, + this.callback + ) + }) + + it('should find the project with only the rootDoc_id fiel', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { rootDoc_id: 1 }) + .should.equal(true) + }) + + it('should update the project rootDoc_id', function() { + return this.ProjectRootDocManager.setRootDocAutomatically + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the project does not exist', function() { + beforeEach(function() { + this.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) + return this.ProjectRootDocManager.ensureRootDocumentIsSet( + this.project_id, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWith(new Error('project not found')) + .should.equal(true) + }) + }) + }) + + return describe('ensureRootDocumentIsValid', function() { + beforeEach(function() { + this.project = {} + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith(2, null, this.project) + this.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() + this.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon + .stub() + .callsArgWith(1, null, this.docPaths) + return (this.ProjectRootDocManager.setRootDocAutomatically = sinon + .stub() + .callsArgWith(1, null)) + }) + + describe('when the root doc is set', function() { + describe('when the root doc is valid', function() { + beforeEach(function() { + this.project.rootDoc_id = 'doc-id-2' + return this.ProjectRootDocManager.ensureRootDocumentIsValid( + this.project_id, + this.callback + ) + }) + + it('should find the project fetching only the rootDoc_id field', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { rootDoc_id: 1 }) + .should.equal(true) + }) + + it('should not try to update the project rootDoc_id', function() { + return this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + false + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the root doc is not valid', function() { + beforeEach(function() { + this.project.rootDoc_id = 'bogus-doc-id' + return this.ProjectRootDocManager.ensureRootDocumentIsValid( + this.project_id, + this.callback + ) + }) + + it('should find the project fetching only the rootDoc_id field', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { rootDoc_id: 1 }) + .should.equal(true) + }) + + it('should null the rootDoc_id field', function() { + return this.ProjectEntityUpdateHandler.setRootDoc + .calledWith(this.project_id, null) + .should.equal(true) + }) + + it('should try to find a new rootDoc', function() { + return this.ProjectRootDocManager.setRootDocAutomatically.called.should.equal( + true + ) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) + + describe('when the root doc is not set', function() { + beforeEach(function() { + return this.ProjectRootDocManager.ensureRootDocumentIsSet( + this.project_id, + this.callback + ) + }) + + it('should find the project fetching only the rootDoc_id fiel', function() { + return this.ProjectGetter.getProject + .calledWith(this.project_id, { rootDoc_id: 1 }) + .should.equal(true) + }) + + it('should update the project rootDoc_id', function() { + return this.ProjectRootDocManager.setRootDocAutomatically + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the project does not exist', function() { + beforeEach(function() { + this.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null) + return this.ProjectRootDocManager.ensureRootDocumentIsSet( + this.project_id, + this.callback + ) + }) + + return it('should call the callback with an error', function() { + return this.callback + .calledWith(new Error('project not found')) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Project/ProjectUpdateHandlerTests.js b/services/web/test/unit/src/Project/ProjectUpdateHandlerTests.js new file mode 100644 index 0000000000..7435841309 --- /dev/null +++ b/services/web/test/unit/src/Project/ProjectUpdateHandlerTests.js @@ -0,0 +1,124 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai').should() +const modulePath = + '../../../../app/src/Features/Project/ProjectUpdateHandler.js' +const SandboxedModule = require('sandboxed-module') + +describe('ProjectUpdateHandler', function() { + before(function() { + this.fakeTime = new Date() + return (this.clock = sinon.useFakeTimers(this.fakeTime.getTime())) + }) + + beforeEach(function() { + let Project + this.ProjectModel = Project = class Project {} + this.ProjectModel.update = sinon.stub().callsArg(3) + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + '../../models/Project': { Project: this.ProjectModel }, + 'logger-sharelatex': { log: sinon.stub() } + } + })) + }) + + after(function() { + return this.clock.restore() + }) + + describe('marking a project as recently updated', function() { + beforeEach(function() { + this.project_id = 'project_id' + this.lastUpdatedAt = 987654321 + return (this.lastUpdatedBy = 'fake-last-updater-id') + }) + + it('should send an update to mongo', function(done) { + return this.handler.markAsUpdated( + this.project_id, + this.lastUpdatedAt, + this.lastUpdatedBy, + err => { + sinon.assert.calledWith( + this.ProjectModel.update, + { + _id: this.project_id, + lastUpdated: { $lt: this.lastUpdatedAt } + }, + { + lastUpdated: this.lastUpdatedAt, + lastUpdatedBy: this.lastUpdatedBy + } + ) + return done() + } + ) + }) + + return it('should set smart fallbacks', function(done) { + return this.handler.markAsUpdated(this.project_id, null, null, err => { + sinon.assert.calledWithMatch( + this.ProjectModel.update, + { + _id: this.project_id, + lastUpdated: { $lt: this.fakeTime } + }, + { + lastUpdated: this.fakeTime, + lastUpdatedBy: null + } + ) + return done() + }) + }) + }) + + describe('markAsOpened', () => + it('should send an update to mongo', function(done) { + const project_id = 'project_id' + return this.handler.markAsOpened(project_id, err => { + const args = this.ProjectModel.update.args[0] + args[0]._id.should.equal(project_id) + const date = args[1].lastOpened + '' + const now = Date.now() + '' + date.substring(0, 5).should.equal(now.substring(0, 5)) + return done() + }) + })) + + describe('markAsInactive', () => + it('should send an update to mongo', function(done) { + const project_id = 'project_id' + return this.handler.markAsInactive(project_id, err => { + const args = this.ProjectModel.update.args[0] + args[0]._id.should.equal(project_id) + args[1].active.should.equal(false) + return done() + }) + })) + + return describe('markAsActive', () => + it('should send an update to mongo', function(done) { + const project_id = 'project_id' + return this.handler.markAsActive(project_id, err => { + const args = this.ProjectModel.update.args[0] + args[0]._id.should.equal(project_id) + args[1].active.should.equal(true) + return done() + }) + })) +}) diff --git a/services/web/test/unit/src/Project/SafePathTests.js b/services/web/test/unit/src/Project/SafePathTests.js new file mode 100644 index 0000000000..c3f9310c77 --- /dev/null +++ b/services/web/test/unit/src/Project/SafePathTests.js @@ -0,0 +1,268 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const { assert } = require('chai') +const should = chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Project/SafePath' +const SandboxedModule = require('sandboxed-module') + +describe('SafePath', function() { + beforeEach(function() { + return (this.SafePath = SandboxedModule.require(modulePath)) + }) + + describe('isCleanFilename', function() { + it('should accept a valid filename "main.tex"', function() { + const result = this.SafePath.isCleanFilename('main.tex') + return result.should.equal(true) + }) + + it('should not accept an empty filename', function() { + const result = this.SafePath.isCleanFilename('') + return result.should.equal(false) + }) + + it('should not accept / anywhere', function() { + const result = this.SafePath.isCleanFilename('foo/bar') + return result.should.equal(false) + }) + + it('should not accept .', function() { + const result = this.SafePath.isCleanFilename('.') + return result.should.equal(false) + }) + + it('should not accept ..', function() { + const result = this.SafePath.isCleanFilename('..') + return result.should.equal(false) + }) + + it('should not accept * anywhere', function() { + const result = this.SafePath.isCleanFilename('foo*bar') + return result.should.equal(false) + }) + + it('should not accept leading whitespace', function() { + const result = this.SafePath.isCleanFilename(' foobar.tex') + return result.should.equal(false) + }) + + it('should not accept trailing whitespace', function() { + const result = this.SafePath.isCleanFilename('foobar.tex ') + return result.should.equal(false) + }) + + it('should not accept leading and trailing whitespace', function() { + const result = this.SafePath.isCleanFilename(' foobar.tex ') + return result.should.equal(false) + }) + + it('should not accept control characters (0-31)', function() { + const result = this.SafePath.isCleanFilename('foo\u0010bar') + return result.should.equal(false) + }) + + it('should not accept control characters (127, delete)', function() { + const result = this.SafePath.isCleanFilename('foo\u007fbar') + return result.should.equal(false) + }) + + it('should not accept control characters (128-159)', function() { + const result = this.SafePath.isCleanFilename('foo\u0080\u0090bar') + return result.should.equal(false) + }) + + it('should not accept surrogate characters (128-159)', function() { + const result = this.SafePath.isCleanFilename('foo\uD800\uDFFFbar') + return result.should.equal(false) + }) + + it('should accept javascript property names', function() { + const result = this.SafePath.isCleanFilename('prototype') + return result.should.equal(true) + }) + + it('should accept javascript property names in the prototype', function() { + const result = this.SafePath.isCleanFilename('hasOwnProperty') + return result.should.equal(true) + }) + + // this test never worked correctly because the spaces are not replaced by underscores in isCleanFilename + // it 'should not accept javascript property names resulting from substitutions', -> + // result = @SafePath.isCleanFilename ' proto ' + // result.should.equal false + + // it 'should not accept a trailing .', -> + // result = @SafePath.isCleanFilename 'hello.' + // result.should.equal false + + return it('should not accept \\', function() { + const result = this.SafePath.isCleanFilename('foo\\bar') + return result.should.equal(false) + }) + }) + + describe('isCleanPath', function() { + it('should accept a valid filename "main.tex"', function() { + const result = this.SafePath.isCleanPath('main.tex') + return result.should.equal(true) + }) + + it('should accept a valid path "foo/main.tex"', function() { + const result = this.SafePath.isCleanPath('foo/main.tex') + return result.should.equal(true) + }) + + it('should accept empty path elements', function() { + const result = this.SafePath.isCleanPath('foo//main.tex') + return result.should.equal(true) + }) + + it('should not accept an empty filename', function() { + const result = this.SafePath.isCleanPath('foo/bar/') + return result.should.equal(false) + }) + + it('should accept a path that starts with a slash', function() { + const result = this.SafePath.isCleanPath('/etc/passwd') + return result.should.equal(true) + }) + + it('should not accept a path that has an asterisk as the 0th element', function() { + const result = this.SafePath.isCleanPath('*/foo/bar') + return result.should.equal(false) + }) + + it('should not accept a path that has an asterisk as a middle element', function() { + const result = this.SafePath.isCleanPath('foo/*/bar') + return result.should.equal(false) + }) + + it('should not accept a path that has an asterisk as the filename', function() { + const result = this.SafePath.isCleanPath('foo/bar/*') + return result.should.equal(false) + }) + + it('should not accept a path that contains an asterisk in the 0th element', function() { + const result = this.SafePath.isCleanPath('f*o/bar/baz') + return result.should.equal(false) + }) + + it('should not accept a path that contains an asterisk in a middle element', function() { + const result = this.SafePath.isCleanPath('foo/b*r/baz') + return result.should.equal(false) + }) + + it('should not accept a path that contains an asterisk in the filename', function() { + const result = this.SafePath.isCleanPath('foo/bar/b*z') + return result.should.equal(false) + }) + + it('should not accept multiple problematic elements', function() { + const result = this.SafePath.isCleanPath('f*o/b*r/b*z') + return result.should.equal(false) + }) + + it('should not accept a problematic path with an empty element', function() { + const result = this.SafePath.isCleanPath('foo//*/bar') + return result.should.equal(false) + }) + + it('should not accept javascript property names', function() { + const result = this.SafePath.isCleanPath('prototype') + return result.should.equal(false) + }) + + it('should not accept javascript property names in the prototype', function() { + const result = this.SafePath.isCleanPath('hasOwnProperty') + return result.should.equal(false) + }) + + return it('should not accept javascript property names resulting from substitutions', function() { + const result = this.SafePath.isCleanPath(' proto ') + return result.should.equal(false) + }) + }) + + describe('isAllowedLength', function() { + it('should accept a valid path "main.tex"', function() { + const result = this.SafePath.isAllowedLength('main.tex') + return result.should.equal(true) + }) + + it('should not accept an extremely long path', function() { + const longPath = new Array(1000).join('/subdir') + '/main.tex' + const result = this.SafePath.isAllowedLength(longPath) + return result.should.equal(false) + }) + + return it('should not accept an empty path', function() { + const result = this.SafePath.isAllowedLength('') + return result.should.equal(false) + }) + }) + + return describe('clean', function() { + it('should not modify a valid filename', function() { + const result = this.SafePath.clean('main.tex') + return result.should.equal('main.tex') + }) + + it('should replace invalid characters with _', function() { + const result = this.SafePath.clean('foo/bar*/main.tex') + return result.should.equal('foo_bar__main.tex') + }) + + it('should replace "." with "_"', function() { + const result = this.SafePath.clean('.') + return result.should.equal('_') + }) + + it('should replace ".." with "__"', function() { + const result = this.SafePath.clean('..') + return result.should.equal('__') + }) + + it('should replace a single trailing space with _', function() { + const result = this.SafePath.clean('foo ') + return result.should.equal('foo_') + }) + + it('should replace a multiple trailing spaces with ___', function() { + const result = this.SafePath.clean('foo ') + return result.should.equal('foo__') + }) + + it('should replace a single leading space with _', function() { + const result = this.SafePath.clean(' foo') + return result.should.equal('_foo') + }) + + it('should replace a multiple leading spaces with ___', function() { + const result = this.SafePath.clean(' foo') + return result.should.equal('__foo') + }) + + it('should prefix javascript property names with @', function() { + const result = this.SafePath.clean('prototype') + return result.should.equal('@prototype') + }) + + return it('should prefix javascript property names in the prototype with @', function() { + const result = this.SafePath.clean('hasOwnProperty') + return result.should.equal('@hasOwnProperty') + }) + }) +}) diff --git a/services/web/test/unit/src/Publishers/PublishersGetterTests.js b/services/web/test/unit/src/Publishers/PublishersGetterTests.js new file mode 100644 index 0000000000..cf7943d01c --- /dev/null +++ b/services/web/test/unit/src/Publishers/PublishersGetterTests.js @@ -0,0 +1,70 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Publishers/PublishersGetter.js' +) + +describe('PublishersGetter', function() { + beforeEach(function() { + this.publisher = { + _id: 'mock-publsiher-id', + slug: 'ieee', + fetchV1Data: sinon.stub() + } + + this.PublishersGetter = SandboxedModule.require(modulePath, { + requires: { + '../User/UserGetter': this.UserGetter, + '../UserMembership/UserMembershipsHandler': (this.UserMembershipsHandler = { + getEntitiesByUser: sinon + .stub() + .callsArgWith(2, null, [this.publisher]) + }), + '../UserMembership/UserMembershipEntityConfigs': (this.UserMembershipEntityConfigs = { + publisher: { + modelName: 'Publisher', + canCreate: true, + fields: { + primaryKey: 'slug' + } + } + }), + 'logger-sharelatex': { + log() { + return console.log(arguments) + }, + err() {} + } + } + }) + + return (this.userId = '12345abcde') + }) + + return describe('getManagedPublishers', () => + it('fetches v1 data before returning publisher list', function(done) { + return this.PublishersGetter.getManagedPublishers(this.userId, function( + error, + publishers + ) { + publishers.length.should.equal(1) + return done() + }) + })) +}) diff --git a/services/web/test/unit/src/Referal/ReferalAllocatorTests.js b/services/web/test/unit/src/Referal/ReferalAllocatorTests.js new file mode 100644 index 0000000000..315193e631 --- /dev/null +++ b/services/web/test/unit/src/Referal/ReferalAllocatorTests.js @@ -0,0 +1,161 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Referal/ReferalAllocator.js' +) + +describe('ReferalAllocator', function() { + beforeEach(function() { + this.ReferalAllocator = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: (this.User = {}) + }, + '../Subscription/FeaturesUpdater': (this.FeaturesUpdater = {}), + 'settings-sharelatex': (this.Settings = {}), + 'logger-sharelatex': { + log() {}, + err() {} + } + } + }) + this.callback = sinon.stub() + this.referal_id = 'referal-id-123' + this.referal_medium = 'twitter' + this.user_id = 'user-id-123' + this.new_user_id = 'new-user-id-123' + this.FeaturesUpdater.refreshFeatures = sinon.stub().yields() + this.User.update = sinon.stub().callsArgWith(3, null) + return (this.User.findOne = sinon + .stub() + .callsArgWith(1, null, { _id: this.user_id })) + }) + + return describe('allocate', function() { + describe('when the referal was a bonus referal', function() { + beforeEach(function() { + this.referal_source = 'bonus' + return this.ReferalAllocator.allocate( + this.referal_id, + this.new_user_id, + this.referal_source, + this.referal_medium, + this.callback + ) + }) + + it('should update the referring user with the refered users id', function() { + return this.User.update + .calledWith( + { + referal_id: this.referal_id + }, + { + $push: { + refered_users: this.new_user_id + }, + $inc: { + refered_user_count: 1 + } + } + ) + .should.equal(true) + }) + + it('find the referring users id', function() { + return this.User.findOne + .calledWith({ referal_id: this.referal_id }) + .should.equal(true) + }) + + it("should refresh the user's subscription", function() { + return this.FeaturesUpdater.refreshFeatures + .calledWith(this.user_id) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('when there is no user for the referal id', function() { + beforeEach(function() { + this.referal_source = 'bonus' + this.referal_id = 'wombat' + this.User.findOne = sinon.stub().callsArgWith(1, null, null) + return this.ReferalAllocator.allocate( + this.referal_id, + this.new_user_id, + this.referal_source, + this.referal_medium, + this.callback + ) + }) + + it('should find the referring users id', function() { + return this.User.findOne + .calledWith({ referal_id: this.referal_id }) + .should.equal(true) + }) + + it('should not update the referring user with the refered users id', function() { + return this.User.update.called.should.equal(false) + }) + + it('should not assign the user a bonus', function() { + return this.FeaturesUpdater.refreshFeatures.called.should.equal(false) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + return describe('when the referal is not a bonus referal', function() { + beforeEach(function() { + this.referal_source = 'public_share' + return this.ReferalAllocator.allocate( + this.referal_id, + this.new_user_id, + this.referal_source, + this.referal_medium, + this.callback + ) + }) + + it('should not update the referring user with the refered users id', function() { + return this.User.update.called.should.equal(false) + }) + + it('find the referring users id', function() { + return this.User.findOne + .calledWith({ referal_id: this.referal_id }) + .should.equal(true) + }) + + it('should not assign the user a bonus', function() { + return this.FeaturesUpdater.refreshFeatures.called.should.equal(false) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Referal/ReferalConnectTests.js b/services/web/test/unit/src/Referal/ReferalConnectTests.js new file mode 100644 index 0000000000..10aa48d832 --- /dev/null +++ b/services/web/test/unit/src/Referal/ReferalConnectTests.js @@ -0,0 +1,154 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Referal/ReferalConnect.js' +) + +describe('Referal connect middle wear', function() { + beforeEach(function() { + return (this.connect = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + it('should take a referal query string and put it on the session if it exists', function(done) { + const req = { + query: { referal: '12345' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_id.should.equal(req.query.referal) + return done() + }) + }) + + it('should not change the referal_id on the session if not in query', function(done) { + const req = { + query: {}, + session: { referal_id: 'same' } + } + return this.connect.use(req, {}, function() { + req.session.referal_id.should.equal('same') + return done() + }) + }) + + it('should take a facebook referal query string and put it on the session if it exists', function(done) { + const req = { + query: { fb_ref: '12345' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_id.should.equal(req.query.fb_ref) + return done() + }) + }) + + it('should map the facebook medium into the session', function(done) { + const req = { + query: { rm: 'fb' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_medium.should.equal('facebook') + return done() + }) + }) + + it('should map the twitter medium into the session', function(done) { + const req = { + query: { rm: 't' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_medium.should.equal('twitter') + return done() + }) + }) + + it('should map the google plus medium into the session', function(done) { + const req = { + query: { rm: 'gp' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_medium.should.equal('google_plus') + return done() + }) + }) + + it('should map the email medium into the session', function(done) { + const req = { + query: { rm: 'e' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_medium.should.equal('email') + return done() + }) + }) + + it('should map the direct medium into the session', function(done) { + const req = { + query: { rm: 'd' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_medium.should.equal('direct') + return done() + }) + }) + + it('should map the bonus source into the session', function(done) { + const req = { + query: { rs: 'b' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_source.should.equal('bonus') + return done() + }) + }) + + it('should map the public share source into the session', function(done) { + const req = { + query: { rs: 'ps' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_source.should.equal('public_share') + return done() + }) + }) + + return it('should map the collaborator invite into the session', function(done) { + const req = { + query: { rs: 'ci' }, + session: {} + } + return this.connect.use(req, {}, function() { + req.session.referal_source.should.equal('collaborator_invite') + return done() + }) + }) +}) diff --git a/services/web/test/unit/src/Referal/ReferalControllerTests.js b/services/web/test/unit/src/Referal/ReferalControllerTests.js new file mode 100644 index 0000000000..185652ff73 --- /dev/null +++ b/services/web/test/unit/src/Referal/ReferalControllerTests.js @@ -0,0 +1,32 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Referal/ReferalController.js' +) + +describe('Referal controller', () => + beforeEach(function() { + return (this.controller = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + })) diff --git a/services/web/test/unit/src/Referal/ReferalFeaturesTests.js b/services/web/test/unit/src/Referal/ReferalFeaturesTests.js new file mode 100644 index 0000000000..796bd10f00 --- /dev/null +++ b/services/web/test/unit/src/Referal/ReferalFeaturesTests.js @@ -0,0 +1,101 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Referal/ReferalFeatures.js' +) + +describe('ReferalFeatures', function() { + beforeEach(function() { + this.ReferalFeatures = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: (this.User = {}) + }, + 'settings-sharelatex': (this.Settings = {}), + 'logger-sharelatex': { + log() {}, + err() {} + } + } + }) + this.callback = sinon.stub() + this.referal_id = 'referal-id-123' + this.referal_medium = 'twitter' + this.user_id = 'user-id-123' + return (this.new_user_id = 'new-user-id-123') + }) + + describe('getBonusFeatures', function() { + beforeEach(function() { + this.refered_user_count = 3 + this.Settings.bonus_features = { + '3': { + collaborators: 3, + dropbox: false, + versioning: false + } + } + const stubbedUser = { + refered_user_count: this.refered_user_count, + features: { collaborators: 1, dropbox: false, versioning: false } + } + + this.User.findOne = sinon.stub().callsArgWith(1, null, stubbedUser) + return this.ReferalFeatures.getBonusFeatures(this.user_id, this.callback) + }) + + it('should get the users number of refered user', function() { + return this.User.findOne + .calledWith({ _id: this.user_id }) + .should.equal(true) + }) + + return it('should call the callback with the features', function() { + return this.callback + .calledWith(null, this.Settings.bonus_features[3]) + .should.equal(true) + }) + }) + + return describe('when the user is not at a bonus level', function() { + beforeEach(function() { + this.refered_user_count = 0 + this.Settings.bonus_features = { + '1': { + collaborators: 3, + dropbox: false, + versioning: false + } + } + this.User.findOne = sinon + .stub() + .callsArgWith(1, null, { refered_user_count: this.refered_user_count }) + return this.ReferalFeatures.getBonusFeatures(this.user_id, this.callback) + }) + + it('should get the users number of refered user', function() { + return this.User.findOne + .calledWith({ _id: this.user_id }) + .should.equal(true) + }) + + return it('should call the callback with no features', function() { + return this.callback.calledWith(null, {}).should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Referal/ReferalHandlerTests.js b/services/web/test/unit/src/Referal/ReferalHandlerTests.js new file mode 100644 index 0000000000..e5c8a82bc8 --- /dev/null +++ b/services/web/test/unit/src/Referal/ReferalHandlerTests.js @@ -0,0 +1,118 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Referal/ReferalHandler.js' +) + +describe('Referal handler', function() { + beforeEach(function() { + this.User = { findById: sinon.stub() } + this.handler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../../models/User': { + User: this.User + } + } + }) + return (this.user_id = '12313') + }) + + return describe('getting refered user_ids', function() { + it('should get the user from mongo and return the refered users array', function(done) { + const user = { + refered_users: ['1234', '312312', '3213129'], + refered_user_count: 3 + } + this.User.findById.callsArgWith(1, null, user) + + return this.handler.getReferedUsers(this.user_id, function( + err, + passedReferedUserIds, + passedReferedUserCount + ) { + passedReferedUserIds.should.deep.equal(user.refered_users) + passedReferedUserCount.should.equal(3) + return done() + }) + }) + + it('should return an empty array if it is not set', function(done) { + const user = {} + this.User.findById.callsArgWith(1, null, user) + + return this.handler.getReferedUsers(this.user_id, function( + err, + passedReferedUserIds, + passedReferedUserCount + ) { + passedReferedUserIds.length.should.equal(0) + return done() + }) + }) + + it('should return a zero count if netither it or the array are set', function(done) { + const user = {} + this.User.findById.callsArgWith(1, null, user) + + return this.handler.getReferedUsers(this.user_id, function( + err, + passedReferedUserIds, + passedReferedUserCount + ) { + passedReferedUserCount.should.equal(0) + return done() + }) + }) + + it('should return the array length if count is not set', function(done) { + const user = { refered_users: ['1234', '312312', '3213129'] } + this.User.findById.callsArgWith(1, null, user) + + return this.handler.getReferedUsers(this.user_id, function( + err, + passedReferedUserIds, + passedReferedUserCount + ) { + passedReferedUserCount.should.equal(3) + return done() + }) + }) + + return it('should return the count if it differs from the array length', function(done) { + const user = { + refered_users: ['1234', '312312', '3213129'], + refered_user_count: 5 + } + this.User.findById.callsArgWith(1, null, user) + + return this.handler.getReferedUsers(this.user_id, function( + err, + passedReferedUserIds, + passedReferedUserCount + ) { + passedReferedUserCount.should.equal(5) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/References/ReferencesControllerTests.js b/services/web/test/unit/src/References/ReferencesControllerTests.js new file mode 100644 index 0000000000..74ea70b84c --- /dev/null +++ b/services/web/test/unit/src/References/ReferencesControllerTests.js @@ -0,0 +1,354 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const sinon = require('sinon') +const { assert } = require('chai') +const modulePath = + '../../../../app/src/Features/References/ReferencesController' +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') + +describe('ReferencesController', function() { + beforeEach(function() { + this.projectId = '2222' + this.controller = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {}, + err() {} + }, + 'settings-sharelatex': (this.settings = { + apis: { web: { url: 'http://some.url' } } + }), + './ReferencesHandler': (this.ReferencesHandler = { + index: sinon.stub(), + indexAll: sinon.stub() + }), + '../Editor/EditorRealTimeController': (this.EditorRealTimeController = { + emitToRoom: sinon.stub() + }) + } + }) + this.req = new MockRequest() + this.req.params.Project_id = this.projectId + this.req.body = { + docIds: (this.docIds = ['aaa', 'bbb']), + shouldBroadcast: false + } + this.res = new MockResponse() + this.res.json = sinon.stub() + this.res.send = sinon.stub() + this.res.sendStatus = sinon.stub() + return (this.fakeResponseData = { + projectId: this.projectId, + keys: ['one', 'two', 'three'] + }) + }) + + describe('indexAll', function() { + beforeEach(function() { + this.req.body = { shouldBroadcast: false } + this.ReferencesHandler.indexAll.callsArgWith( + 1, + null, + this.fakeResponseData + ) + return (this.call = callback => { + this.controller.indexAll(this.req, this.res) + return callback() + }) + }) + + it('should not produce an error', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(0) + this.res.sendStatus.calledWith(500).should.equal(false) + this.res.sendStatus.calledWith(400).should.equal(false) + return done() + }) + }) + + it('should return data', function(done) { + return this.call(() => { + this.res.json.callCount.should.equal(1) + this.res.json.calledWith(this.fakeResponseData).should.equal(true) + return done() + }) + }) + + it('should call ReferencesHandler.indexAll', function(done) { + return this.call(() => { + this.ReferencesHandler.indexAll.callCount.should.equal(1) + this.ReferencesHandler.indexAll + .calledWith(this.projectId) + .should.equal(true) + return done() + }) + }) + + describe('when shouldBroadcast is true', function() { + beforeEach(function() { + this.ReferencesHandler.index.callsArgWith( + 2, + null, + this.fakeResponseData + ) + return (this.req.body.shouldBroadcast = true) + }) + + it('should call EditorRealTimeController.emitToRoom', function(done) { + return this.call(() => { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(0) + this.res.sendStatus.calledWith(500).should.equal(false) + this.res.sendStatus.calledWith(400).should.equal(false) + return done() + }) + }) + + return it('should still return data', function(done) { + return this.call(() => { + this.res.json.callCount.should.equal(1) + this.res.json.calledWith(this.fakeResponseData).should.equal(true) + return done() + }) + }) + }) + + return describe('when shouldBroadcast is false', function() { + beforeEach(function() { + this.ReferencesHandler.index.callsArgWith( + 2, + null, + this.fakeResponseData + ) + return (this.req.body.shouldBroadcast = false) + }) + + it('should not call EditorRealTimeController.emitToRoom', function(done) { + return this.call(() => { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(0) + this.res.sendStatus.calledWith(500).should.equal(false) + this.res.sendStatus.calledWith(400).should.equal(false) + return done() + }) + }) + + return it('should still return data', function(done) { + return this.call(() => { + this.res.json.callCount.should.equal(1) + this.res.json.calledWith(this.fakeResponseData).should.equal(true) + return done() + }) + }) + }) + }) + + describe('there is no data', function() { + beforeEach(function() { + this.ReferencesHandler.indexAll.callsArgWith(1) + return (this.call = callback => { + this.controller.indexAll(this.req, this.res) + return callback() + }) + }) + + it('should not call EditorRealTimeController.emitToRoom', function(done) { + return this.call(() => { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(0) + this.res.sendStatus.calledWith(500).should.equal(false) + this.res.sendStatus.calledWith(400).should.equal(false) + return done() + }) + }) + + return it('should send a response with an empty keys list', function(done) { + return this.call(() => { + this.res.json.called.should.equal(true) + this.res.json + .calledWith({ projectId: this.projectId, keys: [] }) + .should.equal(true) + return done() + }) + }) + }) + + return describe('index', function() { + beforeEach(function() { + return (this.call = callback => { + this.controller.index(this.req, this.res) + return callback() + }) + }) + + describe('with docIds as an array and shouldBroadcast as false', function() { + beforeEach(function() { + return this.ReferencesHandler.index.callsArgWith( + 2, + null, + this.fakeResponseData + ) + }) + + it('should call ReferencesHandler.index', function(done) { + return this.call(() => { + this.ReferencesHandler.index.callCount.should.equal(1) + this.ReferencesHandler.index + .calledWith(this.projectId, this.docIds) + .should.equal(true) + return done() + }) + }) + + it('should return data', function(done) { + return this.call(() => { + this.res.json.callCount.should.equal(1) + this.res.json.calledWith(this.fakeResponseData).should.equal(true) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(0) + this.res.sendStatus.calledWith(500).should.equal(false) + this.res.sendStatus.calledWith(400).should.equal(false) + return done() + }) + }) + + it('should not call EditorRealTimController.emitToRoom', function(done) { + return this.call(() => { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + return done() + }) + }) + + return describe('when ReferencesHandler.index produces an error', function() { + beforeEach(function() { + return this.ReferencesHandler.index.callsArgWith( + 2, + new Error('woops'), + null + ) + }) + + return it('should produce an error response', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(1) + this.res.sendStatus.calledWith(500).should.equal(true) + return done() + }) + }) + }) + }) + + describe('when shouldBroadcast is true', function() { + beforeEach(function() { + this.ReferencesHandler.index.callsArgWith( + 2, + null, + this.fakeResponseData + ) + return (this.req.body.shouldBroadcast = true) + }) + + it('should call EditorRealTimeController.emitToRoom', function(done) { + return this.call(() => { + this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(0) + this.res.sendStatus.calledWith(500).should.equal(false) + this.res.sendStatus.calledWith(400).should.equal(false) + return done() + }) + }) + + return it('should still return data', function(done) { + return this.call(() => { + this.res.json.callCount.should.equal(1) + this.res.json.calledWith(this.fakeResponseData).should.equal(true) + return done() + }) + }) + }) + + describe('with missing docIds', function() { + beforeEach(function() { + return delete this.req.body.docIds + }) + + it('should produce an error response', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(1) + this.res.sendStatus.calledWith(400).should.equal(true) + return done() + }) + }) + + return it('should not call ReferencesHandler.index', function(done) { + return this.call(() => { + this.ReferencesHandler.index.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('with invalid docIds', function() { + beforeEach(function() { + return (this.req.body.docIds = 42) + }) + + it('should produce an error response', function(done) { + return this.call(() => { + this.res.sendStatus.callCount.should.equal(1) + this.res.sendStatus.calledWith(400).should.equal(true) + return done() + }) + }) + + return it('should not call ReferencesHandler.index', function(done) { + return this.call(() => { + this.ReferencesHandler.index.callCount.should.equal(0) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/References/ReferencesHandlerTests.js b/services/web/test/unit/src/References/ReferencesHandlerTests.js new file mode 100644 index 0000000000..664c812f2a --- /dev/null +++ b/services/web/test/unit/src/References/ReferencesHandlerTests.js @@ -0,0 +1,543 @@ +/* eslint-disable + handle-callback-err, + max-len, + mocha/no-identical-title, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const { assert } = require('chai') +const modulePath = '../../../../app/src/Features/References/ReferencesHandler' + +describe('ReferencesHandler', function() { + beforeEach(function() { + this.projectId = '222' + this.fakeProject = { + _id: this.projectId, + owner_ref: (this.fakeOwner = { + _id: 'some_owner', + features: { + references: false + } + }), + rootFolder: [ + { + docs: [ + { name: 'one.bib', _id: 'aaa' }, + { name: 'two.txt', _id: 'bbb' } + ], + folders: [ + { + docs: [{ name: 'three.bib', _id: 'ccc' }], + fileRefs: [{ name: 'four.bib', _id: 'ghg' }], + folders: [] + } + ] + } + ] + } + this.docIds = ['aaa', 'ccc'] + this.handler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {}, + err() {} + }, + 'settings-sharelatex': (this.settings = { + apis: { + references: { url: 'http://some.url/references' }, + docstore: { url: 'http://some.url/docstore' }, + filestore: { url: 'http://some.url/filestore' } + } + }), + request: (this.request = { + get: sinon.stub(), + post: sinon.stub() + }), + '../Project/ProjectGetter': (this.ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, this.fakeProject) + }), + '../User/UserGetter': (this.UserGetter = { + getUser: sinon.stub() + }), + '../DocumentUpdater/DocumentUpdaterHandler': (this.DocumentUpdaterHandler = { + flushDocToMongo: sinon.stub().callsArgWith(2, null) + }) + } + }) + return (this.fakeResponseData = { + projectId: this.projectId, + keys: ['k1', 'k2'] + }) + }) + + describe('index', function() { + beforeEach(function() { + sinon.stub(this.handler, '_findBibDocIds') + sinon.stub(this.handler, '_findBibFileIds') + sinon.stub(this.handler, '_isFullIndex').callsArgWith(1, null, true) + this.request.post.callsArgWith( + 1, + null, + { statusCode: 200 }, + this.fakeResponseData + ) + return (this.call = callback => { + return this.handler.index(this.projectId, this.docIds, callback) + }) + }) + + describe('with docIds as an array', function() { + beforeEach(function() { + return (this.docIds = ['aaa', 'ccc']) + }) + + it('should not call _findBibDocIds', function(done) { + return this.call((err, data) => { + this.handler._findBibDocIds.callCount.should.equal(0) + return done() + }) + }) + + it('should call ProjectGetter.getProject', function(done) { + return this.call((err, data) => { + this.ProjectGetter.getProject.callCount.should.equal(1) + this.ProjectGetter.getProject + .calledWith(this.projectId) + .should.equal(true) + return done() + }) + }) + + it('should not call _findBibDocIds', function(done) { + return this.call((err, data) => { + this.handler._findBibDocIds.callCount.should.equal(0) + return done() + }) + }) + + it('should call DocumentUpdaterHandler.flushDocToMongo', function(done) { + return this.call((err, data) => { + this.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2) + this.docIds.forEach(docId => { + return this.DocumentUpdaterHandler.flushDocToMongo + .calledWith(this.projectId, docId) + .should.equal(true) + }) + return done() + }) + }) + + it('should make a request to references service', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(1) + const arg = this.request.post.firstCall.args[0] + expect(arg.json).to.have.all.keys('docUrls', 'fullIndex') + expect(arg.json.docUrls.length).to.equal(2) + expect(arg.json.fullIndex).to.equal(true) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.equal(null) + return done() + }) + }) + + return it('should return data', function(done) { + return this.call((err, data) => { + expect(data).to.not.equal(null) + expect(data).to.not.equal(undefined) + expect(data).to.equal(this.fakeResponseData) + return done() + }) + }) + }) + + describe('when ProjectGetter.getProject produces an error', function() { + beforeEach(function() { + return this.ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + + return it('should not send request', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when _isFullIndex produces an error', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) + return this.handler._isFullIndex.callsArgWith(1, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + + return it('should not send request', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when flushDocToMongo produces an error', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) + this.handler._isFullIndex.callsArgWith(1, false) + return this.DocumentUpdaterHandler.flushDocToMongo.callsArgWith( + 2, + new Error('woops') + ) + }) + + it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + + return it('should not send request', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when request produces an error', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) + this.handler._isFullIndex.callsArgWith(1, null, false) + this.DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) + return this.request.post.callsArgWith(1, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + }) + + return describe('when request responds with error status', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) + this.handler._isFullIndex.callsArgWith(1, null, false) + return this.request.post.callsArgWith( + 1, + null, + { statusCode: 500 }, + null + ) + }) + + return it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + }) + }) + + describe('indexAll', function() { + beforeEach(function() { + sinon.stub(this.handler, '_findBibDocIds').returns(['aaa', 'ccc']) + sinon.stub(this.handler, '_findBibFileIds').returns(['fff', 'ggg']) + sinon.stub(this.handler, '_isFullIndex').callsArgWith(1, null, true) + this.request.post.callsArgWith( + 1, + null, + { statusCode: 200 }, + this.fakeResponseData + ) + return (this.call = callback => { + return this.handler.indexAll(this.projectId, callback) + }) + }) + + it('should call _findBibDocIds', function(done) { + return this.call((err, data) => { + this.handler._findBibDocIds.callCount.should.equal(1) + this.handler._findBibDocIds + .calledWith(this.fakeProject) + .should.equal(true) + return done() + }) + }) + + it('should call _findBibFileIds', function(done) { + return this.call((err, data) => { + this.handler._findBibDocIds.callCount.should.equal(1) + this.handler._findBibDocIds + .calledWith(this.fakeProject) + .should.equal(true) + return done() + }) + }) + + it('should call DocumentUpdaterHandler.flushDocToMongo', function(done) { + return this.call((err, data) => { + this.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2) + return done() + }) + }) + + it('should make a request to references service', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(1) + const arg = this.request.post.firstCall.args[0] + expect(arg.json).to.have.all.keys('docUrls', 'fullIndex') + expect(arg.json.docUrls.length).to.equal(4) + expect(arg.json.fullIndex).to.equal(true) + return done() + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should return data', function(done) { + return this.call((err, data) => { + expect(data).to.not.equal(null) + expect(data).to.not.equal(undefined) + expect(data).to.equal(this.fakeResponseData) + return done() + }) + }) + + describe('when ProjectGetter.getProject produces an error', function() { + beforeEach(function() { + return this.ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + + return it('should not send request', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when _isFullIndex produces an error', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) + return this.handler._isFullIndex.callsArgWith(1, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + + return it('should not send request', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when flushDocToMongo produces an error', function() { + beforeEach(function() { + this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) + this.handler._isFullIndex.callsArgWith(1, false) + return this.DocumentUpdaterHandler.flushDocToMongo.callsArgWith( + 2, + new Error('woops') + ) + }) + + it('should produce an error', function(done) { + return this.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + return done() + }) + }) + + return it('should not send request', function(done) { + return this.call((err, data) => { + this.request.post.callCount.should.equal(0) + return done() + }) + }) + }) + }) + + describe('_findBibDocIds', function() { + beforeEach(function() { + this.fakeProject = { + rootFolder: [ + { + docs: [ + { name: 'one.bib', _id: 'aaa' }, + { name: 'two.txt', _id: 'bbb' } + ], + folders: [ + { docs: [{ name: 'three.bib', _id: 'ccc' }], folders: [] } + ] + } + ] + } + return (this.expectedIds = ['aaa', 'ccc']) + }) + + it('should select the correct docIds', function() { + const result = this.handler._findBibDocIds(this.fakeProject) + return expect(result).to.deep.equal(this.expectedIds) + }) + + return it('should not error with a non array of folders from dirty data', function() { + this.fakeProject.rootFolder[0].folders[0].folders = {} + const result = this.handler._findBibDocIds(this.fakeProject) + return expect(result).to.deep.equal(this.expectedIds) + }) + }) + + describe('_findBibFileIds', function() { + beforeEach(function() { + this.fakeProject = { + rootFolder: [ + { + docs: [ + { name: 'one.bib', _id: 'aaa' }, + { name: 'two.txt', _id: 'bbb' } + ], + fileRefs: [{ name: 'other.bib', _id: 'ddd' }], + folders: [ + { + docs: [{ name: 'three.bib', _id: 'ccc' }], + fileRefs: [{ name: 'four.bib', _id: 'ghg' }], + folders: [] + } + ] + } + ] + } + return (this.expectedIds = ['ddd', 'ghg']) + }) + + return it('should select the correct docIds', function() { + const result = this.handler._findBibFileIds(this.fakeProject) + return expect(result).to.deep.equal(this.expectedIds) + }) + }) + + return describe('_isFullIndex', function() { + beforeEach(function() { + this.fakeProject = { owner_ref: (this.owner_ref = 'owner-ref-123') } + this.owner = { + features: { + references: false + } + } + this.UserGetter.getUser = sinon.stub() + this.UserGetter.getUser + .withArgs(this.owner_ref, { features: true }) + .yields(null, this.owner) + return (this.call = callback => { + return this.handler._isFullIndex(this.fakeProject, callback) + }) + }) + + describe('with references feature on', function() { + beforeEach(function() { + return (this.owner.features.references = true) + }) + + return it('should return true', function() { + return this.call((err, isFullIndex) => { + expect(err).to.equal(null) + return expect(isFullIndex).to.equal(true) + }) + }) + }) + + describe('with references feature off', function() { + beforeEach(function() { + return (this.owner.features.references = false) + }) + + return it('should return false', function() { + return this.call((err, isFullIndex) => { + expect(err).to.equal(null) + return expect(isFullIndex).to.equal(false) + }) + }) + }) + + return describe('with referencesSearch', function() { + beforeEach(function() { + return (this.owner.features = { + referencesSearch: true, + references: false + }) + }) + + return it('should return true', function() { + return this.call((err, isFullIndex) => { + expect(err).to.equal(null) + return expect(isFullIndex).to.equal(true) + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Security/LoginRateLimiterTests.js b/services/web/test/unit/src/Security/LoginRateLimiterTests.js new file mode 100644 index 0000000000..ece9997fa6 --- /dev/null +++ b/services/web/test/unit/src/Security/LoginRateLimiterTests.js @@ -0,0 +1,135 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const { expect } = require('chai') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Security/LoginRateLimiter' +) + +describe('LoginRateLimiter', function() { + beforeEach(function() { + this.email = 'bob@bob.com' + this.RateLimiter = { + clearRateLimit: sinon.stub(), + addCount: sinon.stub() + } + + return (this.LoginRateLimiter = SandboxedModule.require(modulePath, { + requires: { + '../../infrastructure/RateLimiter': this.RateLimiter + } + })) + }) + + describe('processLoginRequest', function() { + beforeEach(function() { + return (this.RateLimiter.addCount = sinon + .stub() + .callsArgWith(1, null, true)) + }) + + it('should call RateLimiter.addCount', function(done) { + return this.LoginRateLimiter.processLoginRequest( + this.email, + (err, allow) => { + this.RateLimiter.addCount.callCount.should.equal(1) + expect( + this.RateLimiter.addCount.lastCall.args[0].endpointName + ).to.equal('login') + expect( + this.RateLimiter.addCount.lastCall.args[0].subjectName + ).to.equal(this.email) + return done() + } + ) + }) + + describe('when login is allowed', function() { + beforeEach(function() { + return (this.RateLimiter.addCount = sinon + .stub() + .callsArgWith(1, null, true)) + }) + + return it('should call pass allow=true', function(done) { + return this.LoginRateLimiter.processLoginRequest( + this.email, + (err, allow) => { + expect(err).to.equal(null) + expect(allow).to.equal(true) + return done() + } + ) + }) + }) + + describe('when login is blocked', function() { + beforeEach(function() { + return (this.RateLimiter.addCount = sinon + .stub() + .callsArgWith(1, null, false)) + }) + + return it('should call pass allow=false', function(done) { + return this.LoginRateLimiter.processLoginRequest( + this.email, + (err, allow) => { + expect(err).to.equal(null) + expect(allow).to.equal(false) + return done() + } + ) + }) + }) + + return describe('when addCount produces an error', function() { + beforeEach(function() { + return (this.RateLimiter.addCount = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.LoginRateLimiter.processLoginRequest( + this.email, + (err, allow) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + }) + }) + + return describe('recordSuccessfulLogin', function() { + beforeEach(function() { + return (this.RateLimiter.clearRateLimit = sinon + .stub() + .callsArgWith(2, null)) + }) + + return it('should call clearRateLimit', function(done) { + return this.LoginRateLimiter.recordSuccessfulLogin(this.email, () => { + this.RateLimiter.clearRateLimit.callCount.should.equal(1) + this.RateLimiter.clearRateLimit + .calledWith('login', this.email) + .should.equal(true) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Security/OneTimeTokenHandlerTests.js b/services/web/test/unit/src/Security/OneTimeTokenHandlerTests.js new file mode 100644 index 0000000000..5574660d8d --- /dev/null +++ b/services/web/test/unit/src/Security/OneTimeTokenHandlerTests.js @@ -0,0 +1,164 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Security/OneTimeTokenHandler' +) +const { expect } = require('chai') +const Errors = require('../../../../app/src/Features/Errors/Errors') +const tk = require('timekeeper') + +describe('OneTimeTokenHandler', function() { + beforeEach(function() { + tk.freeze(Date.now()) // freeze the time for these tests + this.stubbedToken = 'mock-token' + this.callback = sinon.stub() + return (this.OneTimeTokenHandler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {} + }, + crypto: { + randomBytes: () => this.stubbedToken + }, + '../Errors/Errors': Errors, + '../../infrastructure/mongojs': { + db: (this.db = { tokens: {} }) + } + } + })) + }) + + afterEach(() => tk.reset()) + + describe('getNewToken', function() { + beforeEach(function() { + return (this.db.tokens.insert = sinon.stub().yields()) + }) + + describe('normally', function() { + beforeEach(function() { + return this.OneTimeTokenHandler.getNewToken( + 'password', + 'mock-data-to-store', + this.callback + ) + }) + + it('should insert a generated token with a 1 hour expiry', function() { + return this.db.tokens.insert + .calledWith({ + use: 'password', + token: this.stubbedToken, + createdAt: new Date(), + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + data: 'mock-data-to-store' + }) + .should.equal(true) + }) + + return it('should call the callback with the token', function() { + return this.callback + .calledWith(null, this.stubbedToken) + .should.equal(true) + }) + }) + + return describe('with an optional expiresIn parameter', function() { + beforeEach(function() { + return this.OneTimeTokenHandler.getNewToken( + 'password', + 'mock-data-to-store', + { expiresIn: 42 }, + this.callback + ) + }) + + it('should insert a generated token with a custom expiry', function() { + return this.db.tokens.insert + .calledWith({ + use: 'password', + token: this.stubbedToken, + createdAt: new Date(), + expiresAt: new Date(Date.now() + 42 * 1000), + data: 'mock-data-to-store' + }) + .should.equal(true) + }) + + return it('should call the callback with the token', function() { + return this.callback + .calledWith(null, this.stubbedToken) + .should.equal(true) + }) + }) + }) + + return describe('getValueFromTokenAndExpire', function() { + describe('successfully', function() { + beforeEach(function() { + this.db.tokens.findAndModify = sinon + .stub() + .yields(null, { data: 'mock-data' }) + return this.OneTimeTokenHandler.getValueFromTokenAndExpire( + 'password', + 'mock-token', + this.callback + ) + }) + + it('should expire the token', function() { + return this.db.tokens.findAndModify + .calledWith({ + query: { + use: 'password', + token: 'mock-token', + expiresAt: { $gt: new Date() }, + usedAt: { $exists: false } + }, + update: { + $set: { usedAt: new Date() } + } + }) + .should.equal(true) + }) + + return it('should return the data', function() { + return this.callback.calledWith(null, 'mock-data').should.equal(true) + }) + }) + + return describe('when a valid token is not found', function() { + beforeEach(function() { + this.db.tokens.findAndModify = sinon.stub().yields(null, null) + return this.OneTimeTokenHandler.getValueFromTokenAndExpire( + 'password', + 'mock-token', + this.callback + ) + }) + + return it('should return a NotFoundError', function() { + return this.callback + .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Security/RateLimiterMiddlewareTests.js b/services/web/test/unit/src/Security/RateLimiterMiddlewareTests.js new file mode 100644 index 0000000000..2a7f5a16dd --- /dev/null +++ b/services/web/test/unit/src/Security/RateLimiterMiddlewareTests.js @@ -0,0 +1,178 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Security/RateLimiterMiddleware' +) + +describe('RateLimiterMiddleware', function() { + beforeEach(function() { + this.AuthenticationController = { + getLoggedInUserId: () => { + return __guard__( + __guard__( + this.req != null ? this.req.session : undefined, + x1 => x1.user + ), + x => x._id + ) + } + } + this.RateLimiterMiddleware = SandboxedModule.require(modulePath, { + requires: { + '../../infrastructure/RateLimiter': (this.RateLimiter = {}), + 'logger-sharelatex': (this.logger = { warn: sinon.stub() }), + '../Authentication/AuthenticationController': this + .AuthenticationController + } + }) + this.req = { params: {} } + this.res = { + status: sinon.stub(), + write: sinon.stub(), + end: sinon.stub() + } + return (this.next = sinon.stub()) + }) + + return describe('rateLimit', function() { + beforeEach(function() { + this.rateLimiter = this.RateLimiterMiddleware.rateLimit({ + endpointName: 'test-endpoint', + params: ['project_id', 'doc_id'], + timeInterval: 42, + maxRequests: 12 + }) + return (this.req.params = { + project_id: (this.project_id = 'project-id'), + doc_id: (this.doc_id = 'doc-id') + }) + }) + + describe('when there is no session', function() { + beforeEach(function() { + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + this.req.ip = this.ip = '1.2.3.4' + return this.rateLimiter(this.req, this.res, this.next) + }) + + it('should call the rate limiter backend with the ip address', function() { + return this.RateLimiter.addCount + .calledWith({ + endpointName: 'test-endpoint', + timeInterval: 42, + throttle: 12, + subjectName: `${this.project_id}:${this.doc_id}:${this.ip}` + }) + .should.equal(true) + }) + + return it('should pass on to next()', function() {}) + }) + + describe('when under the rate limit with logged in user', function() { + beforeEach(function() { + this.req.session = { + user: { + _id: (this.user_id = 'user-id') + } + } + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + return this.rateLimiter(this.req, this.res, this.next) + }) + + it('should call the rate limiter backend with the user_id', function() { + return this.RateLimiter.addCount + .calledWith({ + endpointName: 'test-endpoint', + timeInterval: 42, + throttle: 12, + subjectName: `${this.project_id}:${this.doc_id}:${this.user_id}` + }) + .should.equal(true) + }) + + return it('should pass on to next()', function() { + return this.next.called.should.equal(true) + }) + }) + + describe('when under the rate limit with anonymous user', function() { + beforeEach(function() { + this.req.ip = this.ip = '1.2.3.4' + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + return this.rateLimiter(this.req, this.res, this.next) + }) + + it('should call the rate limiter backend with the ip address', function() { + return this.RateLimiter.addCount + .calledWith({ + endpointName: 'test-endpoint', + timeInterval: 42, + throttle: 12, + subjectName: `${this.project_id}:${this.doc_id}:${this.ip}` + }) + .should.equal(true) + }) + + return it('should pass on to next()', function() { + return this.next.called.should.equal(true) + }) + }) + + return describe('when over the rate limit', function() { + beforeEach(function() { + this.req.session = { + user: { + _id: (this.user_id = 'user-id') + } + } + this.RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) + return this.rateLimiter(this.req, this.res, this.next) + }) + + it('should return a 429', function() { + this.res.status.calledWith(429).should.equal(true) + return this.res.end.called.should.equal(true) + }) + + it('should not continue', function() { + return this.next.called.should.equal(false) + }) + + return it('should log a warning', function() { + return this.logger.warn + .calledWith( + { + endpointName: 'test-endpoint', + timeInterval: 42, + throttle: 12, + subjectName: `${this.project_id}:${this.doc_id}:${this.user_id}` + }, + 'rate limit exceeded' + ) + .should.equal(true) + }) + }) + }) +}) + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js b/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js new file mode 100644 index 0000000000..6c4c878292 --- /dev/null +++ b/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js @@ -0,0 +1,341 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = '../../../../app/src/Features/Subscription/FeaturesUpdater' +const { assert } = require('chai') +const { ObjectId } = require('mongoose').Types + +describe('FeaturesUpdater', function() { + beforeEach(function() { + this.user_id = ObjectId().toString() + + return (this.FeaturesUpdater = SandboxedModule.require(modulePath, { + requires: { + './UserFeaturesUpdater': (this.UserFeaturesUpdater = {}), + './SubscriptionLocator': (this.SubscriptionLocator = {}), + './PlansLocator': (this.PlansLocator = {}), + 'logger-sharelatex': { + log() {} + }, + 'settings-sharelatex': (this.Settings = {}), + '../Referal/ReferalFeatures': (this.ReferalFeatures = {}), + './V1SubscriptionManager': (this.V1SubscriptionManager = {}), + '../Institutions/InstitutionsFeatures': (this.InstitutionsFeatures = {}) + } + })) + }) + + describe('refreshFeatures', function() { + beforeEach(function() { + this.V1SubscriptionManager.notifyV1OfFeaturesChange = sinon + .stub() + .yields() + this.UserFeaturesUpdater.updateFeatures = sinon.stub().yields() + this.FeaturesUpdater._getIndividualFeatures = sinon + .stub() + .yields(null, { individual: 'features' }) + this.FeaturesUpdater._getGroupFeatureSets = sinon + .stub() + .yields(null, [{ group: 'features' }, { group: 'features2' }]) + this.InstitutionsFeatures.getInstitutionsFeatures = sinon + .stub() + .yields(null, { institutions: 'features' }) + this.FeaturesUpdater._getV1Features = sinon + .stub() + .yields(null, { v1: 'features' }) + this.ReferalFeatures.getBonusFeatures = sinon + .stub() + .yields(null, { bonus: 'features' }) + this.FeaturesUpdater._mergeFeatures = sinon + .stub() + .returns({ merged: 'features' }) + return (this.callback = sinon.stub()) + }) + + describe('normally', function() { + beforeEach(function() { + return this.FeaturesUpdater.refreshFeatures(this.user_id, this.callback) + }) + + it('should get the individual features', function() { + return this.FeaturesUpdater._getIndividualFeatures + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should get the group features', function() { + return this.FeaturesUpdater._getGroupFeatureSets + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should get the institution features', function() { + return this.InstitutionsFeatures.getInstitutionsFeatures + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should get the v1 features', function() { + return this.FeaturesUpdater._getV1Features + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should get the bonus features', function() { + return this.ReferalFeatures.getBonusFeatures + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should merge from the default features', function() { + return this.FeaturesUpdater._mergeFeatures + .calledWith(this.Settings.defaultFeatures) + .should.equal(true) + }) + + it('should merge the individual features', function() { + return this.FeaturesUpdater._mergeFeatures + .calledWith(sinon.match.any, { individual: 'features' }) + .should.equal(true) + }) + + it('should merge the group features', function() { + this.FeaturesUpdater._mergeFeatures + .calledWith(sinon.match.any, { group: 'features' }) + .should.equal(true) + return this.FeaturesUpdater._mergeFeatures + .calledWith(sinon.match.any, { group: 'features2' }) + .should.equal(true) + }) + + it('should merge the institutions features', function() { + return this.FeaturesUpdater._mergeFeatures + .calledWith(sinon.match.any, { institutions: 'features' }) + .should.equal(true) + }) + + it('should merge the v1 features', function() { + return this.FeaturesUpdater._mergeFeatures + .calledWith(sinon.match.any, { v1: 'features' }) + .should.equal(true) + }) + + it('should merge the bonus features', function() { + return this.FeaturesUpdater._mergeFeatures + .calledWith(sinon.match.any, { bonus: 'features' }) + .should.equal(true) + }) + + it('should update the user with the merged features', function() { + return this.UserFeaturesUpdater.updateFeatures + .calledWith(this.user_id, { merged: 'features' }) + .should.equal(true) + }) + + return it('should notify v1', function() { + return this.V1SubscriptionManager.notifyV1OfFeaturesChange.called.should.equal( + true + ) + }) + }) + + return describe('with notifyV1 == false', function() { + beforeEach(function() { + return this.FeaturesUpdater.refreshFeatures( + this.user_id, + false, + this.callback + ) + }) + + return it('should not notify v1', function() { + return this.V1SubscriptionManager.notifyV1OfFeaturesChange.called.should.equal( + false + ) + }) + }) + }) + + return describe('_mergeFeatures', function() { + it('should prefer priority over standard for compileGroup', function() { + expect( + this.FeaturesUpdater._mergeFeatures( + { + compileGroup: 'priority' + }, + { + compileGroup: 'standard' + } + ) + ).to.deep.equal({ + compileGroup: 'priority' + }) + expect( + this.FeaturesUpdater._mergeFeatures( + { + compileGroup: 'standard' + }, + { + compileGroup: 'priority' + } + ) + ).to.deep.equal({ + compileGroup: 'priority' + }) + expect( + this.FeaturesUpdater._mergeFeatures( + { + compileGroup: 'priority' + }, + { + compileGroup: 'priority' + } + ) + ).to.deep.equal({ + compileGroup: 'priority' + }) + return expect( + this.FeaturesUpdater._mergeFeatures( + { + compileGroup: 'standard' + }, + { + compileGroup: 'standard' + } + ) + ).to.deep.equal({ + compileGroup: 'standard' + }) + }) + + it('should prefer -1 over any other for collaborators', function() { + expect( + this.FeaturesUpdater._mergeFeatures( + { + collaborators: -1 + }, + { + collaborators: 10 + } + ) + ).to.deep.equal({ + collaborators: -1 + }) + expect( + this.FeaturesUpdater._mergeFeatures( + { + collaborators: 10 + }, + { + collaborators: -1 + } + ) + ).to.deep.equal({ + collaborators: -1 + }) + return expect( + this.FeaturesUpdater._mergeFeatures( + { + collaborators: 4 + }, + { + collaborators: 10 + } + ) + ).to.deep.equal({ + collaborators: 10 + }) + }) + + it('should prefer the higher of compileTimeout', function() { + expect( + this.FeaturesUpdater._mergeFeatures( + { + compileTimeout: 20 + }, + { + compileTimeout: 10 + } + ) + ).to.deep.equal({ + compileTimeout: 20 + }) + return expect( + this.FeaturesUpdater._mergeFeatures( + { + compileTimeout: 10 + }, + { + compileTimeout: 20 + } + ) + ).to.deep.equal({ + compileTimeout: 20 + }) + }) + + return it('should prefer the true over false for other keys', function() { + expect( + this.FeaturesUpdater._mergeFeatures( + { + github: true + }, + { + github: false + } + ) + ).to.deep.equal({ + github: true + }) + expect( + this.FeaturesUpdater._mergeFeatures( + { + github: false + }, + { + github: true + } + ) + ).to.deep.equal({ + github: true + }) + expect( + this.FeaturesUpdater._mergeFeatures( + { + github: true + }, + { + github: true + } + ) + ).to.deep.equal({ + github: true + }) + return expect( + this.FeaturesUpdater._mergeFeatures( + { + github: false + }, + { + github: false + } + ) + ).to.deep.equal({ + github: false + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/LimitationsManagerTests.js b/services/web/test/unit/src/Subscription/LimitationsManagerTests.js new file mode 100644 index 0000000000..a8019590df --- /dev/null +++ b/services/web/test/unit/src/Subscription/LimitationsManagerTests.js @@ -0,0 +1,730 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Subscription/LimitationsManager' +) +const Settings = require('settings-sharelatex') + +describe('LimitationsManager', function() { + beforeEach(function() { + this.project = { _id: (this.project_id = 'project-id') } + this.user = { _id: (this.user_id = 'user-id'), features: {} } + this.ProjectGetter = { + getProject: (project_id, fields, callback) => { + if (project_id === this.project_id) { + return callback(null, this.project) + } else { + return callback(null, null) + } + } + } + this.UserGetter = { + getUser: (user_id, filter, callback) => { + if (user_id === this.user_id) { + return callback(null, this.user) + } else { + return callback(null, null) + } + } + } + + this.SubscriptionLocator = { + getUsersSubscription: sinon.stub(), + getSubscription: sinon.stub() + } + + return (this.LimitationsManager = SandboxedModule.require(modulePath, { + requires: { + '../Project/ProjectGetter': this.ProjectGetter, + '../User/UserGetter': this.UserGetter, + './SubscriptionLocator': this.SubscriptionLocator, + 'settings-sharelatex': (this.Settings = {}), + '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../Collaborators/CollaboratorsInviteHandler': (this.CollaboratorsInviteHandler = {}), + './V1SubscriptionManager': (this.V1SubscriptionManager = {}), + 'logger-sharelatex': { + log() {} + } + } + })) + }) + + describe('allowedNumberOfCollaboratorsInProject', function() { + describe('when the project is owned by a user without a subscription', function() { + beforeEach(function() { + this.Settings.defaultFeatures = { collaborators: 23 } + this.project.owner_ref = this.user_id + delete this.user.features + this.callback = sinon.stub() + return this.LimitationsManager.allowedNumberOfCollaboratorsInProject( + this.project_id, + this.callback + ) + }) + + return it('should return the default number', function() { + return this.callback + .calledWith(null, this.Settings.defaultFeatures.collaborators) + .should.equal(true) + }) + }) + + return describe('when the project is owned by a user with a subscription', function() { + beforeEach(function() { + this.project.owner_ref = this.user_id + this.user.features = { collaborators: 21 } + this.callback = sinon.stub() + return this.LimitationsManager.allowedNumberOfCollaboratorsInProject( + this.project_id, + this.callback + ) + }) + + return it('should return the number of collaborators the user is allowed', function() { + return this.callback + .calledWith(null, this.user.features.collaborators) + .should.equal(true) + }) + }) + }) + + describe('allowedNumberOfCollaboratorsForUser', function() { + describe('when the user has no features', function() { + beforeEach(function() { + this.Settings.defaultFeatures = { collaborators: 23 } + delete this.user.features + this.callback = sinon.stub() + return this.LimitationsManager.allowedNumberOfCollaboratorsForUser( + this.user_id, + this.callback + ) + }) + + return it('should return the default number', function() { + return this.callback + .calledWith(null, this.Settings.defaultFeatures.collaborators) + .should.equal(true) + }) + }) + + return describe('when the user has features', function() { + beforeEach(function() { + this.user.features = { collaborators: 21 } + this.callback = sinon.stub() + return this.LimitationsManager.allowedNumberOfCollaboratorsForUser( + this.user_id, + this.callback + ) + }) + + return it('should return the number of collaborators the user is allowed', function() { + return this.callback + .calledWith(null, this.user.features.collaborators) + .should.equal(true) + }) + }) + }) + + describe('canAddXCollaborators', function() { + describe('when the project has fewer collaborators than allowed', function() { + beforeEach(function() { + this.current_number = 1 + this.allowed_number = 2 + this.invite_count = 0 + this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + project_id, + callback + ) => callback(null, this.current_number) + this.CollaboratorsInviteHandler.getInviteCount = ( + project_id, + callback + ) => callback(null, this.invite_count) + sinon.stub( + this.LimitationsManager, + 'allowedNumberOfCollaboratorsInProject', + (project_id, callback) => { + return callback(null, this.allowed_number) + } + ) + this.callback = sinon.stub() + return this.LimitationsManager.canAddXCollaborators( + this.project_id, + 1, + this.callback + ) + }) + + return it('should return true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + describe('when the project has fewer collaborators and invites than allowed', function() { + beforeEach(function() { + this.current_number = 1 + this.allowed_number = 4 + this.invite_count = 1 + this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + project_id, + callback + ) => callback(null, this.current_number) + this.CollaboratorsInviteHandler.getInviteCount = ( + project_id, + callback + ) => callback(null, this.invite_count) + sinon.stub( + this.LimitationsManager, + 'allowedNumberOfCollaboratorsInProject', + (project_id, callback) => { + return callback(null, this.allowed_number) + } + ) + this.callback = sinon.stub() + return this.LimitationsManager.canAddXCollaborators( + this.project_id, + 1, + this.callback + ) + }) + + return it('should return true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function() { + beforeEach(function() { + this.current_number = 1 + this.allowed_number = 2 + this.invite_count = 0 + this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + project_id, + callback + ) => callback(null, this.current_number) + this.CollaboratorsInviteHandler.getInviteCount = ( + project_id, + callback + ) => callback(null, this.invite_count) + sinon.stub( + this.LimitationsManager, + 'allowedNumberOfCollaboratorsInProject', + (project_id, callback) => { + return callback(null, this.allowed_number) + } + ) + this.callback = sinon.stub() + return this.LimitationsManager.canAddXCollaborators( + this.project_id, + 2, + this.callback + ) + }) + + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + + describe('when the project has more collaborators than allowed', function() { + beforeEach(function() { + this.current_number = 3 + this.allowed_number = 2 + this.invite_count = 0 + this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + project_id, + callback + ) => callback(null, this.current_number) + this.CollaboratorsInviteHandler.getInviteCount = ( + project_id, + callback + ) => callback(null, this.invite_count) + sinon.stub( + this.LimitationsManager, + 'allowedNumberOfCollaboratorsInProject', + (project_id, callback) => { + return callback(null, this.allowed_number) + } + ) + this.callback = sinon.stub() + return this.LimitationsManager.canAddXCollaborators( + this.project_id, + 1, + this.callback + ) + }) + + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + + describe('when the project has infinite collaborators', function() { + beforeEach(function() { + this.current_number = 100 + this.allowed_number = -1 + this.invite_count = 0 + this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + project_id, + callback + ) => callback(null, this.current_number) + this.CollaboratorsInviteHandler.getInviteCount = ( + project_id, + callback + ) => callback(null, this.invite_count) + sinon.stub( + this.LimitationsManager, + 'allowedNumberOfCollaboratorsInProject', + (project_id, callback) => { + return callback(null, this.allowed_number) + } + ) + this.callback = sinon.stub() + return this.LimitationsManager.canAddXCollaborators( + this.project_id, + 1, + this.callback + ) + }) + + return it('should return true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + describe('when the project has more invites than allowed', function() { + beforeEach(function() { + this.current_number = 0 + this.allowed_number = 2 + this.invite_count = 2 + this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + project_id, + callback + ) => callback(null, this.current_number) + this.CollaboratorsInviteHandler.getInviteCount = ( + project_id, + callback + ) => callback(null, this.invite_count) + sinon.stub( + this.LimitationsManager, + 'allowedNumberOfCollaboratorsInProject', + (project_id, callback) => { + return callback(null, this.allowed_number) + } + ) + this.callback = sinon.stub() + return this.LimitationsManager.canAddXCollaborators( + this.project_id, + 1, + this.callback + ) + }) + + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + + return describe('when the project has more invites and collaborators than allowed', function() { + beforeEach(function() { + this.current_number = 1 + this.allowed_number = 2 + this.invite_count = 1 + this.CollaboratorsHandler.getInvitedCollaboratorCount = ( + project_id, + callback + ) => callback(null, this.current_number) + this.CollaboratorsInviteHandler.getInviteCount = ( + project_id, + callback + ) => callback(null, this.invite_count) + sinon.stub( + this.LimitationsManager, + 'allowedNumberOfCollaboratorsInProject', + (project_id, callback) => { + return callback(null, this.allowed_number) + } + ) + this.callback = sinon.stub() + return this.LimitationsManager.canAddXCollaborators( + this.project_id, + 1, + this.callback + ) + }) + + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + }) + + describe('userHasV2Subscription', function() { + beforeEach(function() { + return (this.SubscriptionLocator.getUsersSubscription = sinon.stub()) + }) + + it('should return true if the recurly token is set', function(done) { + this.SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, { + recurlySubscription_id: '1234' + }) + return this.LimitationsManager.userHasV2Subscription(this.user, function( + err, + hasSubscription + ) { + hasSubscription.should.equal(true) + return done() + }) + }) + + it('should return false if the recurly token is not set', function(done) { + this.SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) + this.subscription = {} + return this.LimitationsManager.userHasV2Subscription(this.user, function( + err, + hasSubscription + ) { + hasSubscription.should.equal(false) + return done() + }) + }) + + it('should return false if the subscription is undefined', function(done) { + this.SubscriptionLocator.getUsersSubscription.callsArgWith(1) + return this.LimitationsManager.userHasV2Subscription(this.user, function( + err, + hasSubscription + ) { + hasSubscription.should.equal(false) + return done() + }) + }) + + it('should return the subscription', function(done) { + const stubbedSubscription = { freeTrial: {}, token: '' } + this.SubscriptionLocator.getUsersSubscription.callsArgWith( + 1, + null, + stubbedSubscription + ) + return this.LimitationsManager.userHasV2Subscription(this.user, function( + err, + hasSubOrIsGroupMember, + subscription + ) { + subscription.should.deep.equal(stubbedSubscription) + return done() + }) + }) + + return describe('when user has a custom account', function() { + beforeEach(function() { + this.fakeSubscription = { customAccount: true } + return this.SubscriptionLocator.getUsersSubscription.callsArgWith( + 1, + null, + this.fakeSubscription + ) + }) + + it('should return true', function(done) { + return this.LimitationsManager.userHasV2Subscription( + this.user, + function(err, hasSubscription, subscription) { + hasSubscription.should.equal(true) + return done() + } + ) + }) + + return it('should return the subscription', function(done) { + return this.LimitationsManager.userHasV2Subscription( + this.user, + (err, hasSubscription, subscription) => { + subscription.should.deep.equal(this.fakeSubscription) + return done() + } + ) + }) + }) + }) + + describe('userIsMemberOfGroupSubscription', function() { + beforeEach(function() { + return (this.SubscriptionLocator.getMemberSubscriptions = sinon.stub()) + }) + + it('should return false if there are no groups subcriptions', function(done) { + this.SubscriptionLocator.getMemberSubscriptions.callsArgWith(1, null, []) + return this.LimitationsManager.userIsMemberOfGroupSubscription( + this.user, + function(err, isMember) { + isMember.should.equal(false) + return done() + } + ) + }) + + return it('should return true if there are no groups subcriptions', function(done) { + let subscriptions + this.SubscriptionLocator.getMemberSubscriptions.callsArgWith( + 1, + null, + (subscriptions = ['mock-subscription']) + ) + return this.LimitationsManager.userIsMemberOfGroupSubscription( + this.user, + function(err, isMember, retSubscriptions) { + isMember.should.equal(true) + retSubscriptions.should.equal(subscriptions) + return done() + } + ) + }) + }) + + describe('hasPaidSubscription', function() { + beforeEach(function() { + this.LimitationsManager.userIsMemberOfGroupSubscription = sinon + .stub() + .yields(null, false) + this.LimitationsManager.userHasV2Subscription = sinon + .stub() + .yields(null, false) + return (this.LimitationsManager.userHasV1Subscription = sinon + .stub() + .yields(null, false)) + }) + + it('should return true if userIsMemberOfGroupSubscription', function(done) { + this.LimitationsManager.userIsMemberOfGroupSubscription = sinon + .stub() + .yields(null, true) + return this.LimitationsManager.hasPaidSubscription(this.user, function( + err, + hasSubOrIsGroupMember + ) { + hasSubOrIsGroupMember.should.equal(true) + return done() + }) + }) + + it('should return true if userHasV2Subscription', function(done) { + this.LimitationsManager.userHasV2Subscription = sinon + .stub() + .yields(null, true) + return this.LimitationsManager.hasPaidSubscription(this.user, function( + err, + hasSubOrIsGroupMember + ) { + hasSubOrIsGroupMember.should.equal(true) + return done() + }) + }) + + it('should return true if userHasV1Subscription', function(done) { + this.LimitationsManager.userHasV1Subscription = sinon + .stub() + .yields(null, true) + return this.LimitationsManager.hasPaidSubscription(this.user, function( + err, + hasSubOrIsGroupMember + ) { + hasSubOrIsGroupMember.should.equal(true) + return done() + }) + }) + + it('should return false if none are true', function(done) { + return this.LimitationsManager.hasPaidSubscription(this.user, function( + err, + hasSubOrIsGroupMember + ) { + hasSubOrIsGroupMember.should.equal(false) + return done() + }) + }) + + return it('should have userHasSubscriptionOrIsGroupMember alias', function(done) { + return this.LimitationsManager.userHasSubscriptionOrIsGroupMember( + this.user, + function(err, hasSubOrIsGroupMember) { + hasSubOrIsGroupMember.should.equal(false) + return done() + } + ) + }) + }) + + describe('userHasV1OrV2Subscription', function() { + beforeEach(function() { + this.LimitationsManager.userHasV2Subscription = sinon + .stub() + .yields(null, false) + return (this.LimitationsManager.userHasV1Subscription = sinon + .stub() + .yields(null, false)) + }) + + it('should return true if userHasV2Subscription', function(done) { + this.LimitationsManager.userHasV2Subscription = sinon + .stub() + .yields(null, true) + return this.LimitationsManager.userHasV1OrV2Subscription( + this.user, + function(err, hasSub) { + hasSub.should.equal(true) + return done() + } + ) + }) + + it('should return true if userHasV1Subscription', function(done) { + this.LimitationsManager.userHasV1Subscription = sinon + .stub() + .yields(null, true) + return this.LimitationsManager.userHasV1OrV2Subscription( + this.user, + function(err, hasSub) { + hasSub.should.equal(true) + return done() + } + ) + }) + + return it('should return false if none are true', function(done) { + return this.LimitationsManager.userHasV1OrV2Subscription( + this.user, + function(err, hasSub) { + hasSub.should.equal(false) + return done() + } + ) + }) + }) + + describe('hasGroupMembersLimitReached', function() { + beforeEach(function() { + this.subscriptionId = '12312' + return (this.subscription = { + membersLimit: 3, + member_ids: ['', ''], + teamInvites: [ + { email: 'bob@example.com', sentAt: new Date(), token: 'hey' } + ] + }) + }) + + it('should return true if the limit is hit (including members and invites)', function(done) { + this.SubscriptionLocator.getSubscription.callsArgWith( + 1, + null, + this.subscription + ) + return this.LimitationsManager.hasGroupMembersLimitReached( + this.subscriptionId, + function(err, limitReached) { + limitReached.should.equal(true) + return done() + } + ) + }) + + it('should return false if the limit is not hit (including members and invites)', function(done) { + this.subscription.membersLimit = 4 + this.SubscriptionLocator.getSubscription.callsArgWith( + 1, + null, + this.subscription + ) + return this.LimitationsManager.hasGroupMembersLimitReached( + this.subscriptionId, + function(err, limitReached) { + limitReached.should.equal(false) + return done() + } + ) + }) + + return it('should return true if the limit has been exceded (including members and invites)', function(done) { + this.subscription.membersLimit = 2 + this.SubscriptionLocator.getSubscription.callsArgWith( + 1, + null, + this.subscription + ) + return this.LimitationsManager.hasGroupMembersLimitReached( + this.subscriptionId, + function(err, limitReached) { + limitReached.should.equal(true) + return done() + } + ) + }) + }) + + return describe('userHasV1Subscription', function() { + it('should return true if v1 returns has_subscription = true', function(done) { + this.V1SubscriptionManager.getSubscriptionsFromV1 = sinon + .stub() + .yields(null, { has_subscription: true }) + return this.LimitationsManager.userHasV1Subscription( + this.user, + (error, result) => { + this.V1SubscriptionManager.getSubscriptionsFromV1 + .calledWith(this.user_id) + .should.equal(true) + result.should.equal(true) + return done() + } + ) + }) + + it('should return false if v1 returns has_subscription = false', function(done) { + this.V1SubscriptionManager.getSubscriptionsFromV1 = sinon + .stub() + .yields(null, { has_subscription: false }) + return this.LimitationsManager.userHasV1Subscription( + this.user, + (error, result) => { + this.V1SubscriptionManager.getSubscriptionsFromV1 + .calledWith(this.user_id) + .should.equal(true) + result.should.equal(false) + return done() + } + ) + }) + + return it('should return false if v1 returns nothing', function(done) { + this.V1SubscriptionManager.getSubscriptionsFromV1 = sinon + .stub() + .yields(null, null) + return this.LimitationsManager.userHasV1Subscription( + this.user, + (error, result) => { + this.V1SubscriptionManager.getSubscriptionsFromV1 + .calledWith(this.user_id) + .should.equal(true) + result.should.equal(false) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js new file mode 100644 index 0000000000..7346db8f0f --- /dev/null +++ b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js @@ -0,0 +1,1439 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const crypto = require('crypto') +const querystring = require('querystring') +const modulePath = '../../../../app/src/Features/Subscription/RecurlyWrapper' +const SandboxedModule = require('sandboxed-module') +const tk = require('timekeeper') + +const fixtures = { + 'subscriptions/44f83d7cba354d5b84812419f923ea96': + '' + + '' + + ' ' + + ' ' + + ' gold' + + ' Gold plan' + + ' ' + + ' 44f83d7cba354d5b84812419f923ea96' + + ' active' + + ' 800' + + ' EUR' + + ' 1' + + ' 2011-05-27T07:00:00Z' + + ' ' + + ' ' + + ' 2011-06-27T07:00:00Z' + + ' 2011-07-27T07:00:00Z' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ipaddresses' + + ' 10' + + ' 150' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '', + 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5': + '' + + '' + + ' ' + + ' ' + + ' gold' + + ' Gold plan' + + ' ' + + ' 44f83d7cba354d5b84812419f923ea96' + + ' active' + + ' 800' + + ' EUR' + + ' 1' + + ' 2011-05-27T07:00:00Z' + + ' ' + + ' ' + + ' 2011-06-27T07:00:00Z' + + ' 2011-07-27T07:00:00Z' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ipaddresses' + + ' 10' + + ' 150' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '', + 'accounts/104': + '' + + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 104' + + ' active' + + ' ' + + ' verena@example.com' + + ' Verena' + + ' Example' + + ' ' + + ' a92468579e9c4231a6c0031c4716c01d' + + ' 2011-10-25T12:00:00' + + '' +} + +const mockApiRequest = function(options, callback) { + if (fixtures[options.url]) { + return callback(null, { statusCode: 200 }, fixtures[options.url]) + } else { + return callback('Not found', { statusCode: 404 }) + } +} + +describe('RecurlyWrapper', function() { + before(function() { + let RecurlyWrapper + this.settings = { + plans: [ + { + planCode: 'collaborator', + name: 'Collaborator', + features: { + collaborators: -1, + versioning: true + } + } + ], + defaultPlanCode: { + collaborators: 0, + versioning: false + }, + apis: { + recurly: { + apiKey: 'nonsense', + privateKey: 'private_nonsense' + } + } + } + + tk.freeze(Date.now()) // freeze the time for these tests + return (this.RecurlyWrapper = RecurlyWrapper = SandboxedModule.require( + modulePath, + { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + err: sinon.stub(), + error: sinon.stub(), + log: sinon.stub() + }, + request: sinon.stub() + } + } + )) + }) + + after(() => tk.reset()) + + describe('getSubscription', function() { + describe('with proper subscription id', function() { + before(function() { + this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + mockApiRequest + ) + return this.RecurlyWrapper.getSubscription( + '44f83d7cba354d5b84812419f923ea96', + (error, recurlySubscription) => { + return (this.recurlySubscription = recurlySubscription) + } + ) + }) + after(function() { + return this.RecurlyWrapper.apiRequest.restore() + }) + + it('should look up the subscription at the normal API end point', function() { + return this.apiRequest.args[0][0].url.should.equal( + 'subscriptions/44f83d7cba354d5b84812419f923ea96' + ) + }) + + return it('should return the subscription', function() { + return this.recurlySubscription.uuid.should.equal( + '44f83d7cba354d5b84812419f923ea96' + ) + }) + }) + + describe('with ReculyJS token', function() { + before(function() { + this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + mockApiRequest + ) + return this.RecurlyWrapper.getSubscription( + '70db44b10f5f4b238669480c9903f6f5', + { recurlyJsResult: true }, + (error, recurlySubscription) => { + return (this.recurlySubscription = recurlySubscription) + } + ) + }) + after(function() { + return this.RecurlyWrapper.apiRequest.restore() + }) + + it('should return the subscription', function() { + return this.recurlySubscription.uuid.should.equal( + '44f83d7cba354d5b84812419f923ea96' + ) + }) + + return it('should look up the subscription at the RecurlyJS API end point', function() { + return this.apiRequest.args[0][0].url.should.equal( + 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5' + ) + }) + }) + + return describe('with includeAccount', function() { + beforeEach(function() { + this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + mockApiRequest + ) + return this.RecurlyWrapper.getSubscription( + '44f83d7cba354d5b84812419f923ea96', + { includeAccount: true }, + (error, recurlySubscription) => { + return (this.recurlySubscription = recurlySubscription) + } + ) + }) + afterEach(function() { + return this.RecurlyWrapper.apiRequest.restore() + }) + + it('should request the account from the API', function() { + return this.apiRequest.args[1][0].url.should.equal('accounts/104') + }) + + return it('should populate the account attribute', function() { + return this.recurlySubscription.account.account_code.should.equal('104') + }) + }) + }) + + describe('updateSubscription', function() { + beforeEach(function(done) { + this.recurlySubscriptionId = 'subscription-id-123' + this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + (options, callback) => { + this.requestOptions = options + return callback( + null, + {}, + fixtures['subscriptions/44f83d7cba354d5b84812419f923ea96'] + ) + } + ) + return this.RecurlyWrapper.updateSubscription( + this.recurlySubscriptionId, + { plan_code: 'silver', timeframe: 'now' }, + (error, recurlySubscription) => { + this.recurlySubscription = recurlySubscription + return done() + } + ) + }) + afterEach(function() { + return this.RecurlyWrapper.apiRequest.restore() + }) + + it('should send an update request to the API', function() { + this.apiRequest.called.should.equal(true) + this.requestOptions.body.should.equal(`\ + + silver + now +\ +`) + this.requestOptions.url.should.equal( + `subscriptions/${this.recurlySubscriptionId}` + ) + return this.requestOptions.method.should.equal('put') + }) + + return it('should return the updated subscription', function() { + should.exist(this.recurlySubscription) + return this.recurlySubscription.plan.plan_code.should.equal('gold') + }) + }) + + describe('cancelSubscription', function() { + beforeEach(function(done) { + this.recurlySubscriptionId = 'subscription-id-123' + this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + (options, callback) => { + options.url.should.equal( + `subscriptions/${this.recurlySubscriptionId}/cancel` + ) + options.method.should.equal('put') + return callback() + } + ) + return this.RecurlyWrapper.cancelSubscription( + this.recurlySubscriptionId, + done + ) + }) + + afterEach(function() { + return this.RecurlyWrapper.apiRequest.restore() + }) + + it('should send a cancel request to the API', function() { + return this.apiRequest.called.should.equal(true) + }) + + return describe('when the subscription is already cancelled', function() { + beforeEach(function() { + this.RecurlyWrapper.apiRequest.restore() + this.recurlySubscriptionId = 'subscription-id-123' + return (this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + (options, callback) => { + return callback( + new Error('woops'), + {}, + "A canceled subscription can't transition to canceled" + ) + } + )) + }) + + return it('should not produce an error', function(done) { + return this.RecurlyWrapper.cancelSubscription( + this.recurlySubscriptionId, + err => { + expect(err).to.equal(null) + return done() + } + ) + }) + }) + }) + + describe('reactivateSubscription', function() { + beforeEach(function(done) { + this.recurlySubscriptionId = 'subscription-id-123' + this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + (options, callback) => { + options.url.should.equal( + `subscriptions/${this.recurlySubscriptionId}/reactivate` + ) + options.method.should.equal('put') + return callback() + } + ) + return this.RecurlyWrapper.reactivateSubscription( + this.recurlySubscriptionId, + done + ) + }) + + afterEach(function() { + return this.RecurlyWrapper.apiRequest.restore() + }) + + return it('should send a cancel request to the API', function() { + return this.apiRequest.called.should.equal(true) + }) + }) + + describe('redeemCoupon', function() { + beforeEach(function(done) { + this.recurlyAccountId = 'account-id-123' + this.coupon_code = '312321312' + this.apiRequest = sinon.stub( + this.RecurlyWrapper, + 'apiRequest', + (options, callback) => { + options.url.should.equal(`coupons/${this.coupon_code}/redeem`) + options.body + .indexOf(`${this.recurlyAccountId}`) + .should.not.equal(-1) + options.body.indexOf('USD').should.not.equal(-1) + options.method.should.equal('post') + return callback() + } + ) + return this.RecurlyWrapper.redeemCoupon( + this.recurlyAccountId, + this.coupon_code, + done + ) + }) + + afterEach(function() { + return this.RecurlyWrapper.apiRequest.restore() + }) + + return it('should send the request to redem the coupon', function() { + return this.apiRequest.called.should.equal(true) + }) + }) + + describe('_addressToXml', function() { + beforeEach(function() { + return (this.address = { + address1: 'addr_one', + address2: 'addr_two', + country: 'some_country', + state: 'some_state', + postal_code: 'some_zip', + nonsenseKey: 'rubbish' + }) + }) + + return it('should generate the correct xml', function() { + const result = this.RecurlyWrapper._addressToXml(this.address) + return should.equal( + result, + `\ + +addr_one +addr_two +some_country +some_state +some_zip +\n\ +` + ) + }) + }) + + describe('createSubscription', function() { + beforeEach(function() { + this.user = { + _id: 'some_id', + email: 'user@example.com' + } + this.subscriptionDetails = { + currencyCode: 'EUR', + plan_code: 'some_plan_code', + coupon_code: '', + isPaypal: true, + address: { + address1: 'addr_one', + address2: 'addr_two', + country: 'some_country', + state: 'some_state', + zip: 'some_zip' + } + } + this.subscription = {} + this.recurly_token_id = 'a-token-id' + return (this.call = callback => { + return this.RecurlyWrapper.createSubscription( + this.user, + this.subscriptionDetails, + this.recurly_token_id, + callback + ) + }) + }) + + describe('when paypal', function() { + beforeEach(function() { + this.subscriptionDetails.isPaypal = true + this._createPaypalSubscription = sinon.stub( + this.RecurlyWrapper, + '_createPaypalSubscription' + ) + return this._createPaypalSubscription.callsArgWith( + 3, + null, + this.subscription + ) + }) + + afterEach(function() { + return this._createPaypalSubscription.restore() + }) + + it('should not produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.equal(null) + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should produce a subscription object', function(done) { + return this.call((err, sub) => { + expect(sub).to.deep.equal(this.subscription) + return done() + }) + }) + + it('should call _createPaypalSubscription', function(done) { + return this.call((err, sub) => { + this._createPaypalSubscription.callCount.should.equal(1) + return done() + }) + }) + + return describe('when _createPaypalSubscription produces an error', function() { + beforeEach(function() { + return this._createPaypalSubscription.callsArgWith( + 3, + new Error('woops') + ) + }) + + return it('should produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + return describe('when not paypal', function() { + beforeEach(function() { + this.subscriptionDetails.isPaypal = false + this._createCreditCardSubscription = sinon.stub( + this.RecurlyWrapper, + '_createCreditCardSubscription' + ) + return this._createCreditCardSubscription.callsArgWith( + 3, + null, + this.subscription + ) + }) + + afterEach(function() { + return this._createCreditCardSubscription.restore() + }) + + it('should not produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.equal(null) + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should produce a subscription object', function(done) { + return this.call((err, sub) => { + expect(sub).to.deep.equal(this.subscription) + return done() + }) + }) + + it('should call _createCreditCardSubscription', function(done) { + return this.call((err, sub) => { + this._createCreditCardSubscription.callCount.should.equal(1) + return done() + }) + }) + + return describe('when _createCreditCardSubscription produces an error', function() { + beforeEach(function() { + return this._createCreditCardSubscription.callsArgWith( + 3, + new Error('woops') + ) + }) + + return it('should produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + }) + + describe('_createCreditCardSubscription', function() { + beforeEach(function() { + this.user = { + _id: 'some_id', + email: 'user@example.com' + } + this.subscriptionDetails = { + currencyCode: 'EUR', + plan_code: 'some_plan_code', + coupon_code: '', + isPaypal: true, + address: { + address1: 'addr_one', + address2: 'addr_two', + country: 'some_country', + state: 'some_state', + zip: 'some_zip' + } + } + this.subscription = {} + this.recurly_token_id = 'a-token-id' + this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest') + this.response = { statusCode: 200 } + this.body = 'is_bad' + this.apiRequest.callsArgWith(1, null, this.response, this.body) + this._parseSubscriptionXml = sinon.stub( + this.RecurlyWrapper, + '_parseSubscriptionXml' + ) + this._parseSubscriptionXml.callsArgWith(1, null, this.subscription) + return (this.call = callback => { + return this.RecurlyWrapper._createCreditCardSubscription( + this.user, + this.subscriptionDetails, + this.recurly_token_id, + callback + ) + }) + }) + + afterEach(function() { + this.apiRequest.restore() + return this._parseSubscriptionXml.restore() + }) + + it('should not produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should produce a subscription', function(done) { + return this.call((err, sub) => { + expect(sub).to.equal(this.subscription) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, sub) => { + this.apiRequest.callCount.should.equal(1) + return done() + }) + }) + + it('should call _parseSubscriptionXml', function(done) { + return this.call((err, sub) => { + this._parseSubscriptionXml.callCount.should.equal(1) + return done() + }) + }) + + describe('when api request produces an error', function() { + beforeEach(function() { + return this.apiRequest.callsArgWith(1, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, sub) => { + this.apiRequest.callCount.should.equal(1) + return done() + }) + }) + + return it('should not _parseSubscriptionXml', function(done) { + return this.call((err, sub) => { + this._parseSubscriptionXml.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when parse xml produces an error', function() { + beforeEach(function() { + return this._parseSubscriptionXml.callsArgWith(1, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('_createPaypalSubscription', function() { + beforeEach(function() { + this.checkAccountExists = sinon.stub( + this.RecurlyWrapper._paypal, + 'checkAccountExists' + ) + this.createAccount = sinon.stub( + this.RecurlyWrapper._paypal, + 'createAccount' + ) + this.createBillingInfo = sinon.stub( + this.RecurlyWrapper._paypal, + 'createBillingInfo' + ) + this.setAddress = sinon.stub(this.RecurlyWrapper._paypal, 'setAddress') + this.createSubscription = sinon.stub( + this.RecurlyWrapper._paypal, + 'createSubscription' + ) + this.user = { + _id: 'some_id', + email: 'user@example.com' + } + this.subscriptionDetails = { + currencyCode: 'EUR', + plan_code: 'some_plan_code', + coupon_code: '', + isPaypal: true, + address: { + address1: 'addr_one', + address2: 'addr_two', + country: 'some_country', + state: 'some_state', + zip: 'some_zip' + } + } + this.subscription = {} + this.recurly_token_id = 'a-token-id' + + // set up data callbacks + const { user } = this + const { subscriptionDetails } = this + const { recurly_token_id } = this + + this.checkAccountExists.callsArgWith(1, null, { + user, + subscriptionDetails, + recurly_token_id, + userExists: false, + account: { accountCode: 'xx' } + }) + this.createAccount.callsArgWith(1, null, { + user, + subscriptionDetails, + recurly_token_id, + userExists: false, + account: { accountCode: 'xx' } + }) + this.createBillingInfo.callsArgWith(1, null, { + user, + subscriptionDetails, + recurly_token_id, + userExists: false, + account: { accountCode: 'xx' }, + billingInfo: { token_id: 'abc' } + }) + this.setAddress.callsArgWith(1, null, { + user, + subscriptionDetails, + recurly_token_id, + userExists: false, + account: { accountCode: 'xx' }, + billingInfo: { token_id: 'abc' } + }) + this.createSubscription.callsArgWith(1, null, { + user, + subscriptionDetails, + recurly_token_id, + userExists: false, + account: { accountCode: 'xx' }, + billingInfo: { token_id: 'abc' }, + subscription: this.subscription + }) + + return (this.call = callback => { + return this.RecurlyWrapper._createPaypalSubscription( + this.user, + this.subscriptionDetails, + this.recurly_token_id, + callback + ) + }) + }) + + afterEach(function() { + this.checkAccountExists.restore() + this.createAccount.restore() + this.createBillingInfo.restore() + this.setAddress.restore() + return this.createSubscription.restore() + }) + + it('should not produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should produce a subscription object', function(done) { + return this.call((err, sub) => { + expect(sub).to.not.equal(null) + expect(sub).to.equal(this.subscription) + return done() + }) + }) + + it('should call each of the paypal stages', function(done) { + return this.call((err, sub) => { + this.checkAccountExists.callCount.should.equal(1) + this.createAccount.callCount.should.equal(1) + this.createBillingInfo.callCount.should.equal(1) + this.setAddress.callCount.should.equal(1) + this.createSubscription.callCount.should.equal(1) + return done() + }) + }) + + return describe('when one of the paypal stages produces an error', function() { + beforeEach(function() { + return this.createAccount.callsArgWith(1, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, sub) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should stop calling the paypal stages after the error', function(done) { + return this.call((err, sub) => { + this.checkAccountExists.callCount.should.equal(1) + this.createAccount.callCount.should.equal(1) + this.createBillingInfo.callCount.should.equal(0) + this.setAddress.callCount.should.equal(0) + this.createSubscription.callCount.should.equal(0) + return done() + }) + }) + }) + }) + + describe('paypal actions', function() { + beforeEach(function() { + this.apiRequest = sinon.stub(this.RecurlyWrapper, 'apiRequest') + this._parseAccountXml = sinon.spy(this.RecurlyWrapper, '_parseAccountXml') + this._parseBillingInfoXml = sinon.spy( + this.RecurlyWrapper, + '_parseBillingInfoXml' + ) + this._parseSubscriptionXml = sinon.spy( + this.RecurlyWrapper, + '_parseSubscriptionXml' + ) + return (this.cache = { + user: (this.user = { _id: 'some_id' }), + recurly_token_id: (this.recurly_token_id = 'some_token'), + subscriptionDetails: (this.subscriptionDetails = { + currencyCode: 'EUR', + plan_code: 'some_plan_code', + coupon_code: '', + isPaypal: true, + address: { + address1: 'addr_one', + address2: 'addr_two', + country: 'some_country', + state: 'some_state', + zip: 'some_zip' + } + }) + }) + }) + + afterEach(function() { + this.apiRequest.restore() + this._parseAccountXml.restore() + this._parseBillingInfoXml.restore() + return this._parseSubscriptionXml.restore() + }) + + describe('_paypal.checkAccountExists', function() { + beforeEach(function() { + return (this.call = callback => { + return this.RecurlyWrapper._paypal.checkAccountExists( + this.cache, + callback + ) + }) + }) + + describe('when the account exists', function() { + beforeEach(function() { + const resultXml = + 'abc' + return this.apiRequest.callsArgWith( + 1, + null, + { statusCode: 200 }, + resultXml + ) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, result) => { + this.apiRequest.callCount.should.equal(1) + return done() + }) + }) + + it('should call _parseAccountXml', function(done) { + return this.call((err, result) => { + this.RecurlyWrapper._parseAccountXml.callCount.should.equal(1) + return done() + }) + }) + + it('should add the account to the cumulative result', function(done) { + return this.call((err, result) => { + expect(result.account).to.not.equal(null) + expect(result.account).to.not.equal(undefined) + expect(result.account).to.deep.equal({ + account_code: 'abc' + }) + return done() + }) + }) + + return it('should set userExists to true', function(done) { + return this.call((err, result) => { + expect(result.userExists).to.equal(true) + return done() + }) + }) + }) + + describe('when the account does not exist', function() { + beforeEach(function() { + return this.apiRequest.callsArgWith(1, null, { statusCode: 404 }, '') + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, result) => { + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('GET') + return done() + }) + }) + + it('should not call _parseAccountXml', function(done) { + return this.call((err, result) => { + this.RecurlyWrapper._parseAccountXml.callCount.should.equal(0) + return done() + }) + }) + + it('should not add the account to result', function(done) { + return this.call((err, result) => { + expect(result.account).to.equal(undefined) + return done() + }) + }) + + return it('should set userExists to false', function(done) { + return this.call((err, result) => { + expect(result.userExists).to.equal(false) + return done() + }) + }) + }) + + return describe('when apiRequest produces an error', function() { + beforeEach(function() { + return this.apiRequest.callsArgWith(1, new Error('woops'), { + statusCode: 500 + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('_paypal.createAccount', function() { + beforeEach(function() { + return (this.call = callback => { + return this.RecurlyWrapper._paypal.createAccount(this.cache, callback) + }) + }) + + describe('when address is missing from subscriptionDetails', function() { + beforeEach(function() { + return (this.cache.subscriptionDetails.address = null) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + + describe('when account already exists', function() { + beforeEach(function() { + this.cache.userExists = true + return (this.cache.account = { account_code: 'abc' }) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should produce cache object', function(done) { + return this.call((err, result) => { + expect(result).to.deep.equal(this.cache) + expect(result.account).to.deep.equal({ + account_code: 'abc' + }) + return done() + }) + }) + + it('should not call apiRequest', function(done) { + return this.call((err, result) => { + this.apiRequest.callCount.should.equal(0) + return done() + }) + }) + + return it('should not call _parseAccountXml', function(done) { + return this.call((err, result) => { + this.RecurlyWrapper._parseAccountXml.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when account does not exist', function() { + beforeEach(function() { + this.cache.userExists = false + const resultXml = + 'abc' + return this.apiRequest.callsArgWith( + 1, + null, + { statusCode: 200 }, + resultXml + ) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, result) => { + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('POST') + return done() + }) + }) + + it('should call _parseAccountXml', function(done) { + return this.call((err, result) => { + this.RecurlyWrapper._parseAccountXml.callCount.should.equal(1) + return done() + }) + }) + + return describe('when apiRequest produces an error', function() { + beforeEach(function() { + return this.apiRequest.callsArgWith(1, new Error('woops'), { + statusCode: 500 + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + }) + + describe('_paypal.createBillingInfo', function() { + beforeEach(function() { + this.cache.account = { account_code: 'abc' } + return (this.call = callback => { + return this.RecurlyWrapper._paypal.createBillingInfo( + this.cache, + callback + ) + }) + }) + + describe('when account_code is missing from cache', function() { + beforeEach(function() { + return (this.cache.account.account_code = null) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() { + const resultXml = '1' + return this.apiRequest.callsArgWith( + 1, + null, + { statusCode: 200 }, + resultXml + ) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, result) => { + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('POST') + return done() + }) + }) + + it('should call _parseBillingInfoXml', function(done) { + return this.call((err, result) => { + this.RecurlyWrapper._parseBillingInfoXml.callCount.should.equal(1) + return done() + }) + }) + + return it('should set billingInfo on cache', function(done) { + return this.call((err, result) => { + expect(result.billingInfo).to.deep.equal({ + a: '1' + }) + return done() + }) + }) + }) + + return describe('when apiRequest produces an error', function() { + beforeEach(function() { + return this.apiRequest.callsArgWith(1, new Error('woops'), { + statusCode: 500 + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('_paypal.setAddress', function() { + beforeEach(function() { + this.cache.account = { account_code: 'abc' } + this.cache.billingInfo = {} + return (this.call = callback => { + return this.RecurlyWrapper._paypal.setAddress(this.cache, callback) + }) + }) + + describe('when account_code is missing from cache', function() { + beforeEach(function() { + return (this.cache.account.account_code = null) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + + describe('when address is missing from subscriptionDetails', function() { + beforeEach(function() { + return (this.cache.subscriptionDetails.address = null) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() { + const resultXml = 'London' + return this.apiRequest.callsArgWith( + 1, + null, + { statusCode: 200 }, + resultXml + ) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, result) => { + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('PUT') + return done() + }) + }) + + it('should call _parseBillingInfoXml', function(done) { + return this.call((err, result) => { + this.RecurlyWrapper._parseBillingInfoXml.callCount.should.equal(1) + return done() + }) + }) + + return it('should set billingInfo on cache', function(done) { + return this.call((err, result) => { + expect(result.billingInfo).to.deep.equal({ + city: 'London' + }) + return done() + }) + }) + }) + + return describe('when apiRequest produces an error', function() { + beforeEach(function() { + return this.apiRequest.callsArgWith(1, new Error('woops'), { + statusCode: 500 + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + return describe('_paypal.createSubscription', function() { + beforeEach(function() { + this.cache.account = { account_code: 'abc' } + this.cache.billingInfo = {} + return (this.call = callback => { + return this.RecurlyWrapper._paypal.createSubscription( + this.cache, + callback + ) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() { + const resultXml = '1' + return this.apiRequest.callsArgWith( + 1, + null, + { statusCode: 200 }, + resultXml + ) + }) + + it('should not produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should call apiRequest', function(done) { + return this.call((err, result) => { + this.apiRequest.callCount.should.equal(1) + this.apiRequest.firstCall.args[0].method.should.equal('POST') + return done() + }) + }) + + it('should call _parseSubscriptionXml', function(done) { + return this.call((err, result) => { + this.RecurlyWrapper._parseSubscriptionXml.callCount.should.equal(1) + return done() + }) + }) + + return it('should set subscription on cache', function(done) { + return this.call((err, result) => { + expect(result.subscription).to.deep.equal({ + a: '1' + }) + return done() + }) + }) + }) + + return describe('when apiRequest produces an error', function() { + beforeEach(function() { + return this.apiRequest.callsArgWith(1, new Error('woops'), { + statusCode: 500 + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, result) => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + }) + + return describe('listAccountActiveSubscriptions', function() { + beforeEach(function() { + this.user_id = 'mock-user-id' + this.callback = sinon.stub() + this.RecurlyWrapper.apiRequest = sinon + .stub() + .yields( + null, + (this.response = { mock: 'response' }), + (this.body = '') + ) + return (this.RecurlyWrapper._parseSubscriptionsXml = sinon + .stub() + .yields(null, (this.subscriptions = ['mock', 'subscriptions']))) + }) + + describe('with an account', function() { + beforeEach(function() { + return this.RecurlyWrapper.listAccountActiveSubscriptions( + this.user_id, + this.callback + ) + }) + + it('should send a request to Recurly', function() { + return this.RecurlyWrapper.apiRequest + .calledWith({ + url: `accounts/${this.user_id}/subscriptions`, + qs: { + state: 'active' + }, + expect404: true + }) + .should.equal(true) + }) + + return it('should return the subscriptions', function() { + return this.callback + .calledWith(null, this.subscriptions) + .should.equal(true) + }) + }) + + return describe('without an account', function() { + beforeEach(function() { + this.response.statusCode = 404 + return this.RecurlyWrapper.listAccountActiveSubscriptions( + this.user_id, + this.callback + ) + }) + + return it('should return an empty array of subscriptions', function() { + return this.callback.calledWith(null, []).should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js new file mode 100644 index 0000000000..415020e3df --- /dev/null +++ b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js @@ -0,0 +1,641 @@ +/* eslint-disable + max-len, + mocha/handle-done-callback, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const should = require('chai').should() +const { expect } = require('chai') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const modulePath = + '../../../../app/src/Features/Subscription/SubscriptionController' + +const mockSubscriptions = { + 'subscription-123-active': { + uuid: 'subscription-123-active', + plan: { + name: 'Gold', + plan_code: 'gold' + }, + current_period_ends_at: new Date(), + state: 'active', + unit_amount_in_cents: 999, + account: { + account_code: 'user-123' + } + } +} + +describe('SubscriptionController', function() { + beforeEach(function() { + this.user = { + email: 'tom@yahoo.com', + _id: 'one', + signUpDate: new Date('2000-10-01') + } + this.activeRecurlySubscription = + mockSubscriptions['subscription-123-active'] + + this.AuthenticationController = { + getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user), + getLoggedInUserId: sinon.stub().returns(this.user._id), + getSessionUser: sinon.stub().returns(this.user), + isUserLoggedIn: sinon.stub().returns(true) + } + this.SubscriptionHandler = { + createSubscription: sinon.stub().callsArgWith(3), + updateSubscription: sinon.stub().callsArgWith(3), + reactivateSubscription: sinon.stub().callsArgWith(1), + cancelSubscription: sinon.stub().callsArgWith(1), + recurlyCallback: sinon.stub().callsArgWith(1), + startFreeTrial: sinon.stub() + } + + this.PlansLocator = { findLocalPlanInSettings: sinon.stub() } + + this.LimitationsManager = { + hasPaidSubscription: sinon.stub(), + userHasV1OrV2Subscription: sinon.stub(), + userHasV2Subscription: sinon.stub() + } + + this.SubscriptionViewModelBuilder = { + buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}), + buildViewModel: sinon.stub() + } + this.settings = { + coupon_codes: { + upgradeToAnnualPromo: { + student: 'STUDENTCODEHERE', + collaborator: 'COLLABORATORCODEHERE' + } + }, + apis: { + recurly: { + subdomain: 'sl' + } + }, + siteUrl: 'http://de.sharelatex.dev:3000', + gaExperiments: {} + } + this.GeoIpLookup = { getCurrencyCode: sinon.stub() } + this.UserGetter = { getUser: sinon.stub().callsArgWith(2, null, this.user) } + this.SubscriptionController = SandboxedModule.require(modulePath, { + requires: { + '../Authentication/AuthenticationController': this + .AuthenticationController, + './SubscriptionHandler': this.SubscriptionHandler, + './PlansLocator': this.PlansLocator, + './SubscriptionViewModelBuilder': this.SubscriptionViewModelBuilder, + './LimitationsManager': this.LimitationsManager, + '../../infrastructure/GeoIpLookup': this.GeoIpLookup, + 'logger-sharelatex': { + log() {}, + warn() {} + }, + 'settings-sharelatex': this.settings, + '../User/UserGetter': this.UserGetter, + './RecurlyWrapper': (this.RecurlyWrapper = {}), + './FeaturesUpdater': (this.FeaturesUpdater = {}), + './GroupPlansData': (this.GroupPlansData = {}), + './V1SubscriptionManager': (this.V1SubscriptionManager = {}) + } + }) + + this.res = new MockResponse() + this.req = new MockRequest() + this.req.body = {} + this.req.query = { planCode: '123123' } + + return (this.stubbedCurrencyCode = 'GBP') + }) + + describe('plansPage', function() { + beforeEach(function() { + this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534' + return this.GeoIpLookup.getCurrencyCode.callsArgWith( + 1, + null, + this.stubbedCurrencyCode + ) + }) + + describe('when user is logged in', function(done) { + beforeEach(function(done) { + this.res.callback = done + return this.SubscriptionController.plansPage(this.req, this.res) + }) + it('should fetch the current user', function(done) { + this.UserGetter.getUser.callCount.should.equal(1) + return done() + }) + + return describe('not dependant on logged in state', function(done) { + // these could have been put in 'when user is not logged in' too + it('should set the recommended currency from the geoiplookup', function(done) { + this.res.renderedVariables.recomendedCurrency.should.equal( + this.stubbedCurrencyCode + ) + this.GeoIpLookup.getCurrencyCode + .calledWith(this.req.ip) + .should.equal(true) + return done() + }) + return it('should include data for features table', function(done) { + this.res.renderedVariables.planFeatures.length.should.not.equal(0) + return done() + }) + }) + }) + + return describe('when user is not logged in', function(done) { + beforeEach(function(done) { + this.res.callback = done + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(null) + return this.SubscriptionController.plansPage(this.req, this.res) + }) + + return it('should not fetch the current user', function(done) { + this.UserGetter.getUser.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('paymentPage', function() { + beforeEach(function() { + this.req.headers = {} + this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon + .stub() + .yields(null, true) + return this.GeoIpLookup.getCurrencyCode.callsArgWith( + 1, + null, + this.stubbedCurrencyCode + ) + }) + + describe('with a user without a subscription', function() { + beforeEach(function() { + this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( + 1, + null, + false + ) + return this.PlansLocator.findLocalPlanInSettings.returns({}) + }) + + return describe('with a valid plan code', () => + it('should render the new subscription page', function(done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/new') + return done() + } + return this.SubscriptionController.paymentPage(this.req, this.res) + })) + }) + + describe('with a user with subscription', () => + it('should redirect to the subscription dashboard', function(done) { + this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( + 1, + null, + true + ) + this.res.redirect = url => { + url.should.equal('/user/subscription?hasSubscription=true') + return done() + } + return this.SubscriptionController.paymentPage(this.req, this.res) + })) + + describe('with an invalid plan code', () => + it('should redirect to the subscription dashboard', function(done) { + this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( + 1, + null, + false + ) + this.PlansLocator.findLocalPlanInSettings.returns(null) + this.res.redirect = url => { + url.should.equal('/user/subscription?hasSubscription=true') + return done() + } + return this.SubscriptionController.paymentPage(this.req, this.res) + })) + + describe('which currency to use', function() { + beforeEach(function() { + this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( + 1, + null, + false + ) + return this.PlansLocator.findLocalPlanInSettings.returns({}) + }) + + it('should use the set currency from the query string', function(done) { + this.req.query.currency = 'EUR' + this.res.render = (page, opts) => { + opts.currency.should.equal('EUR') + opts.currency.should.not.equal(this.stubbedCurrencyCode) + return done() + } + return this.SubscriptionController.paymentPage(this.req, this.res) + }) + + it('should upercase the currency code', function(done) { + this.req.query.currency = 'eur' + this.res.render = (page, opts) => { + opts.currency.should.equal('EUR') + return done() + } + return this.SubscriptionController.paymentPage(this.req, this.res) + }) + + return it('should use the geo ip currency if non is provided', function(done) { + this.req.query.currency = null + this.res.render = (page, opts) => { + opts.currency.should.equal(this.stubbedCurrencyCode) + return done() + } + return this.SubscriptionController.paymentPage(this.req, this.res) + }) + }) + + return describe('with a recurly subscription already', () => + it('should redirect to the subscription dashboard', function(done) { + this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( + 1, + null, + false + ) + this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon + .stub() + .yields(null, false) + this.res.redirect = url => { + url.should.equal('/user/subscription?hasSubscription=true') + return done() + } + return this.SubscriptionController.paymentPage(this.req, this.res) + })) + }) + + describe('successful_subscription', () => + beforeEach(function(done) { + this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith( + 1, + null, + {} + ) + this.res.callback = done + return this.SubscriptionController.successful_subscription( + this.req, + this.res + ) + })) + + describe('userSubscriptionPage', function() { + beforeEach(function(done) { + this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith( + 1, + null, + { + personalSubscription: (this.personalSubscription = { + 'personal-subscription': 'mock' + }), + memberGroupSubscriptions: (this.memberGroupSubscriptions = { + 'group-subscriptions': 'mock' + }) + } + ) + this.SubscriptionViewModelBuilder.buildViewModel.returns( + (this.plans = { plans: 'mock' }) + ) + this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( + 1, + null, + false + ) + this.res.render = (view, data) => { + this.data = data + expect(view).to.equal('subscriptions/dashboard') + return done() + } + return this.SubscriptionController.userSubscriptionPage( + this.req, + this.res + ) + }) + + it('should load the personal, groups and v1 subscriptions', function() { + expect(this.data.personalSubscription).to.deep.equal( + this.personalSubscription + ) + return expect(this.data.memberGroupSubscriptions).to.deep.equal( + this.memberGroupSubscriptions + ) + }) + + it('should load the user', function() { + return expect(this.data.user).to.deep.equal(this.user) + }) + + return it('should load the plans', function() { + return expect(this.data.plans).to.deep.equal(this.plans) + }) + }) + + describe('createSubscription', function() { + beforeEach(function(done) { + this.res = { + sendStatus() { + return done() + } + } + sinon.spy(this.res, 'sendStatus') + this.subscriptionDetails = { + card: '1234', + cvv: '123' + } + this.req.body.recurly_token_id = '1234' + this.req.body.subscriptionDetails = this.subscriptionDetails + this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false) + return this.SubscriptionController.createSubscription(this.req, this.res) + }) + + it('should send the user and subscriptionId to the handler', function(done) { + this.SubscriptionHandler.createSubscription + .calledWith( + this.user, + this.subscriptionDetails, + this.req.body.recurly_token_id + ) + .should.equal(true) + return done() + }) + + return it('should redurect to the subscription page', function(done) { + this.res.sendStatus.calledWith(201).should.equal(true) + return done() + }) + }) + + describe('updateSubscription via post', function() { + beforeEach(function(done) { + this.res = { + redirect() { + return done() + } + } + sinon.spy(this.res, 'redirect') + this.plan_code = '1234' + this.req.body.plan_code = this.plan_code + return this.SubscriptionController.updateSubscription(this.req, this.res) + }) + + it('should send the user and subscriptionId to the handler', function(done) { + this.SubscriptionHandler.updateSubscription + .calledWith(this.user, this.plan_code) + .should.equal(true) + return done() + }) + + return it('should redurect to the subscription page', function(done) { + this.res.redirect.calledWith('/user/subscription').should.equal(true) + return done() + }) + }) + + describe('reactivateSubscription', function() { + beforeEach(function(done) { + this.res = { + redirect() { + return done() + } + } + sinon.spy(this.res, 'redirect') + return this.SubscriptionController.reactivateSubscription( + this.req, + this.res + ) + }) + + it('should tell the handler to reactivate this user', function(done) { + this.SubscriptionHandler.reactivateSubscription + .calledWith(this.user) + .should.equal(true) + return done() + }) + + return it('should redurect to the subscription page', function(done) { + this.res.redirect.calledWith('/user/subscription').should.equal(true) + return done() + }) + }) + + describe('cancelSubscription', function() { + beforeEach(function(done) { + this.res = { + redirect() { + return done() + } + } + sinon.spy(this.res, 'redirect') + return this.SubscriptionController.cancelSubscription(this.req, this.res) + }) + + it('should tell the handler to cancel this user', function(done) { + this.SubscriptionHandler.cancelSubscription + .calledWith(this.user) + .should.equal(true) + return done() + }) + + return it('should redurect to the subscription page', function(done) { + this.res.redirect + .calledWith('/user/subscription/canceled') + .should.equal(true) + return done() + }) + }) + + describe('recurly callback', function() { + describe('with a actionable request', function() { + beforeEach(function(done) { + this.req = { + body: { + expired_subscription_notification: { + subscription: { + uuid: this.activeRecurlySubscription.uuid + } + } + } + } + this.res = { + sendStatus() { + return done() + } + } + sinon.spy(this.res, 'sendStatus') + return this.SubscriptionController.recurlyCallback(this.req, this.res) + }) + + it('should tell the SubscriptionHandler to process the recurly callback', function(done) { + this.SubscriptionHandler.recurlyCallback.called.should.equal(true) + return done() + }) + + return it('should send a 200', function(done) { + this.res.sendStatus.calledWith(200) + return done() + }) + }) + + return describe('with a non-actionable request', function() { + beforeEach(function(done) { + this.user.id = this.activeRecurlySubscription.account.account_code + this.req = { + body: { + new_subscription_notification: { + subscription: { + uuid: this.activeRecurlySubscription.uuid + } + } + } + } + this.res = { + sendStatus() { + return done() + } + } + sinon.spy(this.res, 'sendStatus') + return this.SubscriptionController.recurlyCallback(this.req, this.res) + }) + + it('should not call the subscriptionshandler', function() { + return this.SubscriptionHandler.recurlyCallback.called.should.equal( + false + ) + }) + + return it('should respond with a 200 status', function() { + return this.res.sendStatus.calledWith(200) + }) + }) + }) + + describe('renderUpgradeToAnnualPlanPage', function() { + it('should redirect to the plans page if the user does not have a subscription', function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false) + this.res.redirect = function(url) { + url.should.equal('/user/subscription/plans') + return done() + } + return this.SubscriptionController.renderUpgradeToAnnualPlanPage( + this.req, + this.res + ) + }) + + it('should pass the plan code to the view - student', function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + true, + { planCode: 'Student free trial 14 days' } + ) + this.res.render = function(view, opts) { + view.should.equal('subscriptions/upgradeToAnnual') + opts.planName.should.equal('student') + return done() + } + return this.SubscriptionController.renderUpgradeToAnnualPlanPage( + this.req, + this.res + ) + }) + + it('should pass the plan code to the view - collaborator', function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + true, + { planCode: 'free trial for Collaborator free trial 14 days' } + ) + this.res.render = function(view, opts) { + opts.planName.should.equal('collaborator') + return done() + } + return this.SubscriptionController.renderUpgradeToAnnualPlanPage( + this.req, + this.res + ) + }) + + return it('should pass annual as the plan name if the user is already on an annual plan', function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + true, + { planCode: 'student annual with free trial' } + ) + this.res.render = function(view, opts) { + opts.planName.should.equal('annual') + return done() + } + return this.SubscriptionController.renderUpgradeToAnnualPlanPage( + this.req, + this.res + ) + }) + }) + + return describe('processUpgradeToAnnualPlan', function() { + beforeEach(function() {}) + + it('should tell the subscription handler to update the subscription with the annual plan and apply a coupon code', function(done) { + this.req.body = { planName: 'student' } + + this.res.sendStatus = () => { + this.SubscriptionHandler.updateSubscription + .calledWith(this.user, 'student-annual', 'STUDENTCODEHERE') + .should.equal(true) + return done() + } + + return this.SubscriptionController.processUpgradeToAnnualPlan( + this.req, + this.res + ) + }) + + return it('should get the collaborator coupon code', function(done) { + this.req.body = { planName: 'collaborator' } + + this.res.sendStatus = url => { + this.SubscriptionHandler.updateSubscription + .calledWith(this.user, 'collaborator-annual', 'COLLABORATORCODEHERE') + .should.equal(true) + return done() + } + + return this.SubscriptionController.processUpgradeToAnnualPlan( + this.req, + this.res + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.js new file mode 100644 index 0000000000..437e84b900 --- /dev/null +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.js @@ -0,0 +1,90 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const sinon = require('sinon') +const { assert } = require('chai') +const modulePath = + '../../../../app/src/Features/Subscription/SubscriptionGroupController' +const MockResponse = require('../helpers/MockResponse') + +describe('SubscriptionGroupController', function() { + beforeEach(function() { + this.user = { _id: '!@312431', email: 'user@email.com' } + this.adminUserId = '123jlkj' + this.subscriptionId = '123434325412' + this.user_email = 'bob@gmail.com' + this.req = { + session: { + user: { + _id: this.adminUserId, + email: this.user_email + } + }, + params: { + subscriptionId: this.subscriptionId + }, + query: {} + } + + this.subscription = { + _id: this.subscriptionId + } + + this.GroupHandler = { removeUserFromGroup: sinon.stub().callsArgWith(2) } + + this.SubscriptionLocator = { + findManagedSubscription: sinon + .stub() + .callsArgWith(1, null, this.subscription) + } + + this.AuthenticationController = { + getLoggedInUserId(req) { + return req.session.user._id + }, + getSessionUser(req) { + return req.session.user + } + } + + return (this.Controller = SandboxedModule.require(modulePath, { + requires: { + './SubscriptionGroupHandler': this.GroupHandler, + 'logger-sharelatex': { + log() {} + }, + './SubscriptionLocator': this.SubscriptionLocator, + '../Authentication/AuthenticationController': this + .AuthenticationController + } + })) + }) + + return describe('removeUserFromGroup', () => + it('should use the subscription id for the logged in user and take the user id from the params', function(done) { + const userIdToRemove = '31231' + this.req.params = { user_id: userIdToRemove } + this.req.entity = this.subscription + + const res = { + send: () => { + this.GroupHandler.removeUserFromGroup + .calledWith(this.subscriptionId, userIdToRemove) + .should.equal(true) + return done() + } + } + return this.Controller.removeUserFromGroup(this.req, res) + })) +}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js new file mode 100644 index 0000000000..85e01e3b59 --- /dev/null +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js @@ -0,0 +1,248 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-dupe-keys, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const sinon = require('sinon') +const { assert } = require('chai') +const modulePath = + '../../../../app/src/Features/Subscription/SubscriptionGroupHandler' + +describe('SubscriptionGroupHandler', function() { + beforeEach(function() { + this.adminUser_id = '12321' + this.newEmail = 'bob@smith.com' + this.user_id = '3121321' + this.email = 'jim@example.com' + this.user = { _id: this.user_id, email: this.newEmail } + this.subscription_id = '31DSd1123D' + + this.subscription = { + admin_id: this.adminUser_id, + manager_ids: [this.adminUser_id], + _id: this.subscription_id + } + + this.SubscriptionLocator = { + getUsersSubscription: sinon.stub(), + getSubscriptionByMemberIdAndId: sinon.stub(), + getSubscription: sinon.stub().callsArgWith(1, null, this.subscription) + } + + this.UserCreator = { + getUserOrCreateHoldingAccount: sinon + .stub() + .callsArgWith(1, null, this.user) + } + + this.SubscriptionUpdater = { + removeUserFromGroup: sinon.stub().callsArgWith(2), + getSubscription: sinon.stub().callsArgWith(2) + } + + this.TeamInvitesHandler = { createInvite: sinon.stub().callsArgWith(2) } + + this.UserGetter = { + getUser: sinon.stub(), + getUserByAnyEmail: sinon.stub() + } + + this.LimitationsManager = { hasGroupMembersLimitReached: sinon.stub() } + + this.OneTimeTokenHandler = { + getValueFromTokenAndExpire: sinon.stub(), + getNewToken: sinon.stub() + } + + this.EmailHandler = { sendEmail: sinon.stub() } + + this.Subscription = { + update: sinon.stub().yields(), + findOne: sinon.stub().yields() + } + + this.settings = { siteUrl: 'http://www.sharelatex.com' } + + this.readStub = sinon.stub() + this.NotificationsBuilder = { + groupPlan: sinon.stub().returns({ read: this.readStub }) + } + + this.UserMembershipViewModel = { + build(email) { + return { email } + } + } + + return (this.Handler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {} + }, + '../User/UserCreator': this.UserCreator, + './SubscriptionUpdater': this.SubscriptionUpdater, + './SubscriptionLocator': this.SubscriptionLocator, + '../../models/Subscription': { + Subscription: this.Subscription + }, + '../User/UserGetter': this.UserGetter, + './LimitationsManager': this.LimitationsManager, + '../Security/OneTimeTokenHandler': this.OneTimeTokenHandler, + '../Email/EmailHandler': this.EmailHandler, + 'settings-sharelatex': this.settings, + '../Notifications/NotificationsBuilder': this.NotificationsBuilder, + '../UserMembership/UserMembershipViewModel': this + .UserMembershipViewModel, + 'logger-sharelatex': { + err() {}, + log() {}, + warn() {} + } + } + })) + }) + + describe('removeUserFromGroup', () => + it('should call the subscription updater to remove the user', function(done) { + return this.Handler.removeUserFromGroup( + this.adminUser_id, + this.user._id, + err => { + this.SubscriptionUpdater.removeUserFromGroup + .calledWith(this.adminUser_id, this.user._id) + .should.equal(true) + return done() + } + ) + })) + + describe('replaceUserReferencesInGroups', function() { + beforeEach(function(done) { + this.oldId = 'ba5eba11' + this.newId = '5ca1ab1e' + return this.Handler.replaceUserReferencesInGroups( + this.oldId, + this.newId, + () => done() + ) + }) + + it('replaces the admin_id', function() { + return this.Subscription.update + .calledWith({ admin_id: this.oldId }, { admin_id: this.newId }) + .should.equal(true) + }) + + it('replaces the manager_ids', function() { + this.Subscription.update + .calledWith( + { manager_ids: 'ba5eba11' }, + { $addToSet: { manager_ids: '5ca1ab1e' } }, + { multi: true } + ) + .should.equal(true) + + return this.Subscription.update + .calledWith( + { manager_ids: 'ba5eba11' }, + { $pull: { manager_ids: 'ba5eba11' } }, + { multi: true } + ) + .should.equal(true) + }) + + return it('replaces the member ids', function() { + this.Subscription.update + .calledWith( + { member_ids: this.oldId }, + { $addToSet: { member_ids: this.newId } } + ) + .should.equal(true) + + return this.Subscription.update + .calledWith( + { member_ids: this.oldId }, + { $pull: { member_ids: this.oldId } } + ) + .should.equal(true) + }) + }) + + describe('isUserPartOfGroup', function() { + beforeEach(function() { + return (this.subscription_id = '123ed13123') + }) + + it('should return true when user is part of subscription', function(done) { + this.SubscriptionLocator.getSubscriptionByMemberIdAndId.callsArgWith( + 2, + null, + { _id: this.subscription_id } + ) + return this.Handler.isUserPartOfGroup( + this.user_id, + this.subscription_id, + function(err, partOfGroup) { + partOfGroup.should.equal(true) + return done() + } + ) + }) + + return it('should return false when no subscription is found', function(done) { + this.SubscriptionLocator.getSubscriptionByMemberIdAndId.callsArgWith( + 2, + null + ) + return this.Handler.isUserPartOfGroup( + this.user_id, + this.subscription_id, + function(err, partOfGroup) { + partOfGroup.should.equal(false) + return done() + } + ) + }) + }) + + return describe('getTotalConfirmedUsersInGroup', function() { + describe('for existing subscriptions', function() { + beforeEach(function() { + return (this.subscription.member_ids = ['12321', '3121321']) + }) + return it('should call the subscription locator and return 2 users', function(done) { + return this.Handler.getTotalConfirmedUsersInGroup( + this.subscription_id, + (err, count) => { + this.SubscriptionLocator.getSubscription + .calledWith(this.subscription_id) + .should.equal(true) + count.should.equal(2) + return done() + } + ) + }) + }) + return describe('for nonexistent subscriptions', () => + it('should return undefined', function(done) { + return this.Handler.getTotalConfirmedUsersInGroup( + 'fake-id', + (err, count) => { + should.not.exist(count) + return done() + } + ) + })) + }) +}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js new file mode 100644 index 0000000000..0e1e542364 --- /dev/null +++ b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js @@ -0,0 +1,475 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const sinon = require('sinon') +const querystring = require('querystring') +const modulePath = + '../../../../app/src/Features/Subscription/SubscriptionHandler' + +const mockRecurlySubscriptions = { + 'subscription-123-active': { + uuid: 'subscription-123-active', + plan: { + name: 'Gold', + plan_code: 'gold' + }, + current_period_ends_at: new Date(), + state: 'active', + unit_amount_in_cents: 999, + account: { + account_code: 'user-123' + } + } +} + +describe('SubscriptionHandler', function() { + beforeEach(function() { + this.Settings = { + plans: [ + { + planCode: 'collaborator', + name: 'Collaborator', + features: { + collaborators: -1, + versioning: true + } + } + ], + defaultPlanCode: { + collaborators: 0, + versioning: false + } + } + this.activeRecurlySubscription = + mockRecurlySubscriptions['subscription-123-active'] + this.User = {} + this.user = { _id: (this.user_id = 'user_id_here_') } + this.subscription = { + recurlySubscription_id: this.activeRecurlySubscription.uuid + } + this.RecurlyWrapper = { + getSubscription: sinon + .stub() + .callsArgWith(2, null, this.activeRecurlySubscription), + updateSubscription: sinon + .stub() + .callsArgWith(2, null, this.activeRecurlySubscription), + cancelSubscription: sinon.stub().callsArgWith(1), + reactivateSubscription: sinon.stub().callsArgWith(1), + redeemCoupon: sinon.stub().callsArgWith(2), + createSubscription: sinon + .stub() + .callsArgWith(3, null, this.activeRecurlySubscription) + } + + this.DropboxHandler = { unlinkAccount: sinon.stub().callsArgWith(1) } + + this.SubscriptionUpdater = { + syncSubscription: sinon.stub().callsArgWith(2), + startFreeTrial: sinon.stub().callsArgWith(1) + } + + this.LimitationsManager = { userHasV2Subscription: sinon.stub() } + + this.EmailHandler = { sendEmail: sinon.stub() } + + this.AnalyticsManager = { recordEvent: sinon.stub() } + + this.SubscriptionHandler = SandboxedModule.require(modulePath, { + requires: { + './RecurlyWrapper': this.RecurlyWrapper, + 'settings-sharelatex': this.Settings, + '../../models/User': { + User: this.User + }, + './SubscriptionUpdater': this.SubscriptionUpdater, + 'logger-sharelatex': { log() {} }, + './LimitationsManager': this.LimitationsManager, + '../Email/EmailHandler': this.EmailHandler, + '../Dropbox/DropboxHandler': this.DropboxHandler, + '../../infrastructure/Events': (this.Events = { emit: sinon.stub() }), + '../Analytics/AnalyticsManager': this.AnalyticsManager + } + }) + + return (this.SubscriptionHandler.syncSubscriptionToUser = sinon + .stub() + .callsArgWith(2)) + }) + + describe('createSubscription', function() { + beforeEach(function() { + this.callback = sinon.stub() + this.subscriptionDetails = { + cvv: '123', + number: '12345' + } + this.recurly_token_id = '45555666' + return (this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon + .stub() + .yields(null, true)) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.SubscriptionHandler.createSubscription( + this.user, + this.subscriptionDetails, + this.recurly_token_id, + this.callback + ) + }) + + it('should create the subscription with the wrapper', function() { + return this.RecurlyWrapper.createSubscription + .calledWith( + this.user, + this.subscriptionDetails, + this.recurly_token_id + ) + .should.equal(true) + }) + + return it('should sync the subscription to the user', function() { + this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true) + this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal( + this.activeRecurlySubscription + ) + return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal( + this.user._id + ) + }) + }) + + return describe('when there is already a subscription in Recurly', function() { + beforeEach(function() { + this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon + .stub() + .yields(null, false) + return this.SubscriptionHandler.createSubscription( + this.user, + this.subscriptionDetails, + this.recurly_token_id, + this.callback + ) + }) + + return it('should return an error', function() { + return this.callback.calledWith( + new Error('user already has subscription in recurly') + ) + }) + }) + }) + + describe('updateSubscription', function() { + describe('with a user with a subscription', () => + describe('with a valid plan code', function() { + beforeEach(function(done) { + this.plan_code = 'collaborator' + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + true, + this.subscription + ) + return this.SubscriptionHandler.updateSubscription( + this.user, + this.plan_code, + null, + done + ) + }) + + it('should update the subscription', function() { + this.RecurlyWrapper.updateSubscription + .calledWith(this.subscription.recurlySubscription_id) + .should.equal(true) + const updateOptions = this.RecurlyWrapper.updateSubscription + .args[0][1] + return updateOptions.plan_code.should.equal(this.plan_code) + }) + + it('should update immediately', function() { + const updateOptions = this.RecurlyWrapper.updateSubscription + .args[0][1] + return updateOptions.timeframe.should.equal('now') + }) + + return it('should sync the new subscription to the user', function() { + this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal( + true + ) + this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal( + this.activeRecurlySubscription + ) + return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal( + this.user._id + ) + }) + })) + + describe('with a user without a subscription', function() { + beforeEach(function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + false + ) + return this.SubscriptionHandler.updateSubscription( + this.user, + this.plan_code, + null, + done + ) + }) + + return it('should redirect to the subscription dashboard', function() { + this.RecurlyWrapper.updateSubscription.called.should.equal(false) + return this.SubscriptionHandler.syncSubscriptionToUser.called.should.equal( + false + ) + }) + }) + + return describe('with a coupon code', function() { + beforeEach(function(done) { + this.plan_code = 'collaborator' + this.coupon_code = '1231312' + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + true, + this.subscription + ) + return this.SubscriptionHandler.updateSubscription( + this.user, + this.plan_code, + this.coupon_code, + done + ) + }) + + it('should get the users account', function() { + return this.RecurlyWrapper.getSubscription + .calledWith(this.activeRecurlySubscription.uuid) + .should.equal(true) + }) + + it('should redeme the coupon', function(done) { + this.RecurlyWrapper.redeemCoupon + .calledWith( + this.activeRecurlySubscription.account.account_code, + this.coupon_code + ) + .should.equal(true) + return done() + }) + + return it('should update the subscription', function() { + this.RecurlyWrapper.updateSubscription + .calledWith(this.subscription.recurlySubscription_id) + .should.equal(true) + const updateOptions = this.RecurlyWrapper.updateSubscription.args[0][1] + return updateOptions.plan_code.should.equal(this.plan_code) + }) + }) + }) + + describe('cancelSubscription', function() { + describe('with a user without a subscription', function() { + beforeEach(function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + false, + this.subscription + ) + return this.SubscriptionHandler.cancelSubscription(this.user, done) + }) + + return it('should redirect to the subscription dashboard', function() { + return this.RecurlyWrapper.cancelSubscription.called.should.equal(false) + }) + }) + + return describe('with a user with a subscription', function() { + beforeEach(function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + true, + this.subscription + ) + return this.SubscriptionHandler.cancelSubscription(this.user, done) + }) + + it('should cancel the subscription', function() { + this.RecurlyWrapper.cancelSubscription.called.should.equal(true) + return this.RecurlyWrapper.cancelSubscription + .calledWith(this.subscription.recurlySubscription_id) + .should.equal(true) + }) + + return it('should trigger the cancel subscription event', function() { + return this.Events.emit + .calledWith('cancelSubscription', this.user._id) + .should.equal(true) + }) + }) + }) + + describe('reactiveRecurlySubscription', function() { + describe('with a user without a subscription', function() { + beforeEach(function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + false, + this.subscription + ) + return this.SubscriptionHandler.reactivateSubscription(this.user, done) + }) + + it('should redirect to the subscription dashboard', function() { + return this.RecurlyWrapper.reactivateSubscription.called.should.equal( + false + ) + }) + + return it('should not send a notification email', function() { + return sinon.assert.notCalled(this.EmailHandler.sendEmail) + }) + }) + + return describe('with a user with a subscription', function() { + beforeEach(function(done) { + this.LimitationsManager.userHasV2Subscription.callsArgWith( + 1, + null, + true, + this.subscription + ) + return this.SubscriptionHandler.reactivateSubscription(this.user, done) + }) + + it('should reactivate the subscription', function() { + this.RecurlyWrapper.reactivateSubscription.called.should.equal(true) + return this.RecurlyWrapper.reactivateSubscription + .calledWith(this.subscription.recurlySubscription_id) + .should.equal(true) + }) + + return it('should send a notification email', function() { + return sinon.assert.calledWith( + this.EmailHandler.sendEmail, + 'reactivatedSubscription' + ) + }) + }) + }) + + describe('recurlyCallback', () => + describe('with an actionable request', function() { + beforeEach(function(done) { + this.user.id = this.activeRecurlySubscription.account.account_code + + this.User.findById = (userId, callback) => { + userId.should.equal(this.user.id) + return callback(null, this.user) + } + return this.SubscriptionHandler.recurlyCallback( + this.activeRecurlySubscription, + done + ) + }) + + it('should request the affected subscription from the API', function() { + return this.RecurlyWrapper.getSubscription + .calledWith(this.activeRecurlySubscription.uuid) + .should.equal(true) + }) + + it('should request the account details of the subscription', function() { + const options = this.RecurlyWrapper.getSubscription.args[0][1] + return options.includeAccount.should.equal(true) + }) + + return it('should sync the subscription to the user', function() { + this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true) + this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal( + this.activeRecurlySubscription + ) + return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal( + this.user._id + ) + }) + })) + + return describe('validateNoSubscriptionInRecurly', function() { + beforeEach(function() { + this.subscriptions = [] + this.RecurlyWrapper.listAccountActiveSubscriptions = sinon + .stub() + .yields(null, this.subscriptions) + this.SubscriptionUpdater.syncSubscription = sinon.stub().yields() + return (this.callback = sinon.stub()) + }) + + describe('with no subscription in recurly', function() { + beforeEach(function() { + this.subscriptions.push((this.subscription = { mock: 'subscription' })) + return this.SubscriptionHandler.validateNoSubscriptionInRecurly( + this.user_id, + this.callback + ) + }) + + it('should call RecurlyWrapper.listAccountActiveSubscriptions with the user id', function() { + return this.RecurlyWrapper.listAccountActiveSubscriptions + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should sync the subscription', function() { + return this.SubscriptionUpdater.syncSubscription + .calledWith(this.subscription, this.user_id) + .should.equal(true) + }) + + return it('should call the callback with valid == false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + + return describe('with a subscription in recurly', function() { + beforeEach(function() { + return this.SubscriptionHandler.validateNoSubscriptionInRecurly( + this.user_id, + this.callback + ) + }) + + it('should not sync the subscription', function() { + return this.SubscriptionUpdater.syncSubscription.called.should.equal( + false + ) + }) + + return it('should call the callback with valid == true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionLocatorTests.js b/services/web/test/unit/src/Subscription/SubscriptionLocatorTests.js new file mode 100644 index 0000000000..a0c78ae321 --- /dev/null +++ b/services/web/test/unit/src/Subscription/SubscriptionLocatorTests.js @@ -0,0 +1,97 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const sinon = require('sinon') +const modulePath = + '../../../../app/src/Features/Subscription/SubscriptionLocator' +const { assert } = require('chai') +const { ObjectId } = require('mongoose').Types + +describe('Subscription Locator Tests', function() { + beforeEach(function() { + this.user = { _id: '5208dd34438842e2db333333' } + this.subscription = { hello: 'world' } + this.Subscription = { + findOne: sinon.stub(), + find: sinon.stub() + } + return (this.SubscriptionLocator = SandboxedModule.require(modulePath, { + requires: { + '../../models/Subscription': { + Subscription: this.Subscription + }, + 'logger-sharelatex': { + log() {} + } + } + })) + }) + + return describe('finding users subscription', function() { + it('should send the users features', function(done) { + this.Subscription.findOne.callsArgWith(1, null, this.subscription) + return this.SubscriptionLocator.getUsersSubscription( + this.user, + (err, subscription) => { + this.Subscription.findOne + .calledWith({ admin_id: this.user._id }) + .should.equal(true) + subscription.should.equal(this.subscription) + return done() + } + ) + }) + + it('should error if not found', function(done) { + this.Subscription.findOne.callsArgWith(1, 'not found') + return this.SubscriptionLocator.getUsersSubscription( + this.user, + (err, subscription) => { + err.should.exist + return done() + } + ) + }) + + it('should take a user id rather than the user object', function(done) { + this.Subscription.findOne.callsArgWith(1, null, this.subscription) + return this.SubscriptionLocator.getUsersSubscription( + this.user._id, + (err, subscription) => { + this.Subscription.findOne + .calledWith({ admin_id: this.user._id }) + .should.equal(true) + subscription.should.equal(this.subscription) + return done() + } + ) + }) + + return describe('finding managed subscription', () => + it('should query the database', function(done) { + this.Subscription.findOne.callsArgWith(1, null, this.subscription) + return this.SubscriptionLocator.findManagedSubscription( + this.user._id, + (err, subscription) => { + this.Subscription.findOne + .calledWith({ manager_ids: this.user._id }) + .should.equal(true) + subscription.should.equal(this.subscription) + return done() + } + ) + })) + }) +}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js new file mode 100644 index 0000000000..564be5d1e0 --- /dev/null +++ b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js @@ -0,0 +1,453 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = + '../../../../app/src/Features/Subscription/SubscriptionUpdater' +const { assert } = require('chai') +const { ObjectId } = require('mongoose').Types + +describe('SubscriptionUpdater', function() { + beforeEach(function() { + let subscription + this.recurlySubscription = { + uuid: '1238uoijdasjhd', + plan: { + plan_code: 'kjhsakjds' + } + } + this.adminUser = { _id: (this.adminuser_id = '5208dd34438843e2db000007') } + this.otherUserId = '5208dd34438842e2db000005' + this.allUserIds = ['13213', 'dsadas', 'djsaiud89'] + this.userStub = { + _id: 'mock-user-stub-id', + email: 'mock-stub-email@baz.com' + } + this.subscription = subscription = { + _id: '111111111111111111111111', + admin_id: this.adminUser._id, + manager_ids: [this.adminUser._id], + member_ids: this.allUserIds, + save: sinon.stub().callsArgWith(0), + planCode: 'student_or_something' + } + this.user_id = this.adminuser_id + + this.groupSubscription = { + _id: '222222222222222222222222', + admin_id: this.adminUser._id, + manager_ids: [this.adminUser._id], + member_ids: this.allUserIds, + save: sinon.stub().callsArgWith(0), + planCode: 'group_subscription' + } + + this.updateStub = sinon.stub().callsArgWith(2, null) + this.updateManyStub = sinon.stub().callsArgWith(2, null) + this.findAndModifyStub = sinon + .stub() + .callsArgWith(2, null, this.subscription) + this.SubscriptionModel = (function() { + const Cls = class { + static initClass() { + this.remove = sinon.stub().yields() + } + constructor(opts) { + subscription.admin_id = opts.admin_id + subscription.manager_ids = [opts.admin_id] + return subscription + } + } + Cls.initClass() + return Cls + })() + this.SubscriptionModel.update = this.updateStub + this.SubscriptionModel.updateMany = this.updateManyStub + this.SubscriptionModel.findAndModify = this.findAndModifyStub + + this.SubscriptionLocator = { + getUsersSubscription: sinon.stub(), + getGroupSubscriptionMemberOf: sinon.stub(), + getMemberSubscriptions: sinon.stub().yields(null, []) + } + + this.Settings = { + defaultPlanCode: 'personal', + defaultFeatures: { default: 'features' } + } + + this.UserFeaturesUpdater = { updateFeatures: sinon.stub().yields() } + + this.PlansLocator = { findLocalPlanInSettings: sinon.stub().returns({}) } + + this.UserGetter = { + getUsers(memberIds, projection, callback) { + const users = memberIds.map(id => ({ _id: id })) + return callback(null, users) + }, + getUserOrUserStubById: sinon.stub() + } + + this.ReferalFeatures = { getBonusFeatures: sinon.stub().callsArgWith(1) } + this.Modules = { hooks: { fire: sinon.stub().callsArgWith(2, null, null) } } + return (this.SubscriptionUpdater = SandboxedModule.require(modulePath, { + requires: { + '../../models/Subscription': { + Subscription: this.SubscriptionModel + }, + './UserFeaturesUpdater': this.UserFeaturesUpdater, + './SubscriptionLocator': this.SubscriptionLocator, + '../User/UserGetter': this.UserGetter, + './PlansLocator': this.PlansLocator, + 'logger-sharelatex': { + log() {} + }, + 'settings-sharelatex': this.Settings, + './FeaturesUpdater': (this.FeaturesUpdater = {}) + } + })) + }) + + describe('syncSubscription', function() { + beforeEach(function() { + this.SubscriptionLocator.getUsersSubscription.callsArgWith( + 1, + null, + this.subscription + ) + return (this.SubscriptionUpdater._updateSubscriptionFromRecurly = sinon + .stub() + .callsArgWith(2)) + }) + + it('should update the subscription if the user already is admin of one', function(done) { + this.SubscriptionUpdater._createNewSubscription = sinon.stub() + + return this.SubscriptionUpdater.syncSubscription( + this.recurlySubscription, + this.adminUser._id, + err => { + this.SubscriptionLocator.getUsersSubscription + .calledWith(this.adminUser._id) + .should.equal(true) + this.SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal( + true + ) + this.SubscriptionUpdater._updateSubscriptionFromRecurly + .calledWith(this.recurlySubscription, this.subscription) + .should.equal(true) + return done() + } + ) + }) + + return it('should not call updateFeatures with group subscription if recurly subscription is not expired', function(done) { + return this.SubscriptionUpdater.syncSubscription( + this.recurlySubscription, + this.adminUser._id, + err => { + this.SubscriptionLocator.getUsersSubscription + .calledWith(this.adminUser._id) + .should.equal(true) + this.SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal( + true + ) + this.SubscriptionUpdater._updateSubscriptionFromRecurly + .calledWith(this.recurlySubscription, this.subscription) + .should.equal(true) + this.UserFeaturesUpdater.updateFeatures.called.should.equal(false) + return done() + } + ) + }) + }) + + describe('_updateSubscriptionFromRecurly', function() { + beforeEach(function() { + this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) + return (this.SubscriptionUpdater.deleteSubscription = sinon + .stub() + .yields()) + }) + + it('should update the subscription with token etc when not expired', function(done) { + return this.SubscriptionUpdater._updateSubscriptionFromRecurly( + this.recurlySubscription, + this.subscription, + err => { + this.subscription.recurlySubscription_id.should.equal( + this.recurlySubscription.uuid + ) + this.subscription.planCode.should.equal( + this.recurlySubscription.plan.plan_code + ) + this.subscription.save.called.should.equal(true) + this.FeaturesUpdater.refreshFeatures + .calledWith(this.adminUser._id) + .should.equal(true) + return done() + } + ) + }) + + it('should remove the subscription when expired', function(done) { + this.recurlySubscription.state = 'expired' + return this.SubscriptionUpdater._updateSubscriptionFromRecurly( + this.recurlySubscription, + this.subscription, + err => { + this.SubscriptionUpdater.deleteSubscription + .calledWith(this.subscription._id) + .should.equal(true) + return done() + } + ) + }) + + it('should update all the users features', function(done) { + return this.SubscriptionUpdater._updateSubscriptionFromRecurly( + this.recurlySubscription, + this.subscription, + err => { + this.FeaturesUpdater.refreshFeatures + .calledWith(this.adminUser._id) + .should.equal(true) + this.FeaturesUpdater.refreshFeatures + .calledWith(this.allUserIds[0]) + .should.equal(true) + this.FeaturesUpdater.refreshFeatures + .calledWith(this.allUserIds[1]) + .should.equal(true) + this.FeaturesUpdater.refreshFeatures + .calledWith(this.allUserIds[2]) + .should.equal(true) + return done() + } + ) + }) + + it('should set group to true and save how many members can be added to group', function(done) { + this.PlansLocator.findLocalPlanInSettings + .withArgs(this.recurlySubscription.plan.plan_code) + .returns({ groupPlan: true, membersLimit: 5 }) + return this.SubscriptionUpdater._updateSubscriptionFromRecurly( + this.recurlySubscription, + this.subscription, + err => { + this.subscription.membersLimit.should.equal(5) + this.subscription.groupPlan.should.equal(true) + return done() + } + ) + }) + + return it('should not set group to true or set groupPlan', function(done) { + return this.SubscriptionUpdater._updateSubscriptionFromRecurly( + this.recurlySubscription, + this.subscription, + err => { + assert.notEqual(this.subscription.membersLimit, 5) + assert.notEqual(this.subscription.groupPlan, true) + return done() + } + ) + }) + }) + + describe('_createNewSubscription', () => + it('should create a new subscription then update the subscription', function(done) { + return this.SubscriptionUpdater._createNewSubscription( + this.adminUser._id, + () => { + this.subscription.admin_id.should.equal(this.adminUser._id) + this.subscription.manager_ids.should.deep.equal([this.adminUser._id]) + this.subscription.save.called.should.equal(true) + return done() + } + ) + })) + + describe('addUserToGroup', function() { + beforeEach(function() { + return (this.SubscriptionUpdater.addUsersToGroup = sinon + .stub() + .yields(null)) + }) + + return it('delegates to addUsersToGroup', function(done) { + return this.SubscriptionUpdater.addUserToGroup( + this.subscription._id, + this.otherUserId, + () => { + this.SubscriptionUpdater.addUsersToGroup + .calledWith(this.subscription._id, [this.otherUserId]) + .should.equal(true) + return done() + } + ) + }) + }) + + describe('addUsersToGroup', function() { + beforeEach(function() { + return (this.FeaturesUpdater.refreshFeatures = sinon + .stub() + .callsArgWith(1)) + }) + + it('should add the user ids to the group as a set', function(done) { + return this.SubscriptionUpdater.addUsersToGroup( + this.subscription._id, + [this.otherUserId], + () => { + const searchOps = { _id: this.subscription._id } + const insertOperation = { + $addToSet: { member_ids: { $each: [this.otherUserId] } } + } + this.findAndModifyStub + .calledWith(searchOps, insertOperation) + .should.equal(true) + return done() + } + ) + }) + + return it('should update the users features', function(done) { + return this.SubscriptionUpdater.addUserToGroup( + this.subscription._id, + this.otherUserId, + () => { + this.FeaturesUpdater.refreshFeatures + .calledWith(this.otherUserId) + .should.equal(true) + return done() + } + ) + }) + }) + + describe('removeUserFromGroups', function() { + beforeEach(function() { + this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) + this.UserGetter.getUserOrUserStubById.yields(null, {}, false) + this.fakeSubscriptions = [{ _id: 'fake-id-1' }, { _id: 'fake-id-2' }] + return this.SubscriptionLocator.getMemberSubscriptions.yields( + null, + this.fakeSubscriptions + ) + }) + + it('should pull the users id from the group', function(done) { + return this.SubscriptionUpdater.removeUserFromGroup( + this.subscription._id, + this.otherUserId, + () => { + const searchOps = { _id: this.subscription._id } + const removeOperation = { $pull: { member_ids: this.otherUserId } } + this.updateManyStub + .calledWith(searchOps, removeOperation) + .should.equal(true) + return done() + } + ) + }) + + it('should pull the users id from all groups', function(done) { + return this.SubscriptionUpdater.removeUserFromAllGroups( + this.otherUserId, + () => { + const filter = { _id: ['fake-id-1', 'fake-id-2'] } + const removeOperation = { $pull: { member_ids: this.otherUserId } } + sinon.assert.calledWith(this.updateManyStub, filter, removeOperation) + return done() + } + ) + }) + + it('should update the users features', function(done) { + return this.SubscriptionUpdater.removeUserFromGroup( + this.subscription._id, + this.otherUserId, + () => { + this.FeaturesUpdater.refreshFeatures + .calledWith(this.otherUserId) + .should.equal(true) + return done() + } + ) + }) + + return it('should not update features for user stubs', function(done) { + this.UserGetter.getUserOrUserStubById.yields(null, {}, true) + return this.SubscriptionUpdater.removeUserFromGroup( + this.subscription._id, + this.userStub._id, + () => { + this.FeaturesUpdater.refreshFeatures.called.should.equal(false) + return done() + } + ) + }) + }) + + return describe('deleteSubscription', function() { + beforeEach(function(done) { + this.subscription_id = ObjectId().toString() + this.subscription = { + mock: 'subscription', + admin_id: ObjectId(), + member_ids: [ObjectId(), ObjectId(), ObjectId()] + } + this.SubscriptionLocator.getSubscription = sinon + .stub() + .yields(null, this.subscription) + this.FeaturesUpdater.refreshFeatures = sinon.stub().yields() + return this.SubscriptionUpdater.deleteSubscription( + this.subscription_id, + done + ) + }) + + it('should look up the subscription', function() { + return this.SubscriptionLocator.getSubscription + .calledWith(this.subscription_id) + .should.equal(true) + }) + + it('should remove the subscription', function() { + return this.SubscriptionModel.remove + .calledWith({ _id: ObjectId(this.subscription_id) }) + .should.equal(true) + }) + + it('should downgrade the admin_id', function() { + return this.FeaturesUpdater.refreshFeatures + .calledWith(this.subscription.admin_id) + .should.equal(true) + }) + + return it('should downgrade all of the members', function() { + return Array.from(this.subscription.member_ids).map(user_id => + this.FeaturesUpdater.refreshFeatures + .calledWith(user_id) + .should.equal(true) + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js b/services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js new file mode 100644 index 0000000000..32991cc727 --- /dev/null +++ b/services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js @@ -0,0 +1,413 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const sinon = require('sinon') +const { expect } = require('chai') +const querystring = require('querystring') +const modulePath = + '../../../../app/src/Features/Subscription/TeamInvitesHandler' + +const { ObjectId } = require('mongojs') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('TeamInvitesHandler', function() { + beforeEach(function() { + this.manager = { + id: '666666', + first_name: 'Daenerys', + last_name: 'Targaryen', + email: 'daenerys@example.com' + } + + this.token = 'aaaaaaaaaaaaaaaaaaaaaa' + + this.teamInvite = { + email: 'jorah@example.com', + token: this.token + } + + this.subscription = { + id: '55153a8014829a865bbf700d', + _id: new ObjectId('55153a8014829a865bbf700d'), + admin_id: this.manager.id, + groupPlan: true, + member_ids: [], + teamInvites: [this.teamInvite], + save: sinon.stub().yields(null) + } + + this.SubscriptionLocator = { + getUsersSubscription: sinon.stub(), + getSubscription: sinon.stub().yields(null, this.subscription) + } + + this.UserGetter = { + getUser: sinon.stub().yields(), + getUserByAnyEmail: sinon.stub().yields() + } + + this.SubscriptionUpdater = { + addUserToGroup: sinon.stub().yields() + } + + this.LimitationsManager = { + teamHasReachedMemberLimit: sinon.stub().returns(false) + } + + this.Subscription = { + findOne: sinon.stub().yields(), + update: sinon.stub().yields() + } + + this.EmailHandler = { + sendEmail: sinon.stub().yields(null) + } + + this.newToken = 'bbbbbbbbb' + + this.crypto = { + randomBytes: () => { + return { toString: sinon.stub().returns(this.newToken) } + } + } + + this.UserGetter.getUser.withArgs(this.manager.id).yields(null, this.manager) + this.UserGetter.getUserByAnyEmail + .withArgs(this.manager.email) + .yields(null, this.manager) + + this.SubscriptionLocator.getUsersSubscription.yields( + null, + this.subscription + ) + this.Subscription.findOne.yields(null, this.subscription) + + return (this.TeamInvitesHandler = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { log() {} }, + crypto: this.crypto, + 'settings-sharelatex': { siteUrl: 'http://example.com' }, + '../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) }, + '../../models/Subscription': { Subscription: this.Subscription }, + '../User/UserGetter': this.UserGetter, + './SubscriptionLocator': this.SubscriptionLocator, + './SubscriptionUpdater': this.SubscriptionUpdater, + './LimitationsManager': this.LimitationsManager, + '../Email/EmailHandler': this.EmailHandler, + '../Errors/Errors': Errors + } + })) + }) + + describe('getInvite', function() { + it("returns the invite if there's one", function(done) { + return this.TeamInvitesHandler.getInvite( + this.token, + (err, invite, subscription) => { + expect(err).to.eq(null) + expect(invite).to.deep.eq(this.teamInvite) + expect(subscription).to.deep.eq(this.subscription) + return done() + } + ) + }) + + return it("returns teamNotFound if there's none", function(done) { + this.Subscription.findOne = sinon.stub().yields(null, null) + + return this.TeamInvitesHandler.getInvite(this.token, function( + err, + invite, + subscription + ) { + expect(err).to.be.instanceof(Errors.NotFoundError) + return done() + }) + }) + }) + + describe('createInvite', function() { + it('adds the team invite to the subscription', function(done) { + return this.TeamInvitesHandler.createInvite( + this.manager.id, + this.subscription, + 'John.Snow@example.com', + (err, invite) => { + expect(err).to.eq(null) + expect(invite.token).to.eq(this.newToken) + expect(invite.email).to.eq('john.snow@example.com') + expect(invite.inviterName).to.eq( + 'Daenerys Targaryen (daenerys@example.com)' + ) + expect(this.subscription.teamInvites).to.deep.include(invite) + return done() + } + ) + }) + + it('sends an email', function(done) { + return this.TeamInvitesHandler.createInvite( + this.manager.id, + this.subscription, + 'John.Snow@example.com', + (err, invite) => { + this.EmailHandler.sendEmail + .calledWith( + 'verifyEmailToJoinTeam', + sinon.match({ + to: 'john.snow@example.com', + inviterName: 'Daenerys Targaryen (daenerys@example.com)', + acceptInviteUrl: `http://example.com/subscription/invites/${ + this.newToken + }/` + }) + ) + .should.equal(true) + return done() + } + ) + }) + + it('refreshes the existing invite if the email has already been invited', function(done) { + const originalInvite = Object.assign({}, this.teamInvite) + + return this.TeamInvitesHandler.createInvite( + this.manager.id, + this.subscription, + originalInvite.email, + (err, invite) => { + expect(err).to.eq(null) + expect(invite).to.exist + + expect(this.subscription.teamInvites.length).to.eq(1) + expect(this.subscription.teamInvites).to.deep.include(invite) + + expect(invite.email).to.eq(originalInvite.email) + + this.subscription.save.calledOnce.should.eq(true) + + return done() + } + ) + }) + + return it('removes any legacy invite from the subscription', function(done) { + return this.TeamInvitesHandler.createInvite( + this.manager.id, + this.subscription, + 'John.Snow@example.com', + (err, invite) => { + this.Subscription.update + .calledWith( + { _id: new ObjectId('55153a8014829a865bbf700d') }, + { $pull: { invited_emails: 'john.snow@example.com' } } + ) + .should.eq(true) + return done() + } + ) + }) + }) + + describe('importInvite', function() { + beforeEach(function() { + return (this.sentAt = new Date()) + }) + + return it('can imports an invite from v1', function() { + return this.TeamInvitesHandler.importInvite( + this.subscription, + 'A-Team', + 'hannibal@a-team.org', + 'secret', + this.sentAt, + error => { + expect(error).not.to.exist + + this.subscription.save.calledOnce.should.eq(true) + + const invite = this.subscription.teamInvites.find( + i => i.email === 'hannibal@a-team.org' + ) + expect(invite.token).to.eq('secret') + return expect(invite.sentAt).to.eq(this.sentAt) + } + ) + }) + }) + + describe('acceptInvite', function() { + beforeEach(function() { + this.user = { + id: '123456789', + first_name: 'Tyrion', + last_name: 'Lannister', + email: 'tyrion@example.com' + } + + this.UserGetter.getUserByAnyEmail + .withArgs(this.user.email) + .yields(null, this.user) + + return this.subscription.teamInvites.push({ + email: 'john.snow@example.com', + token: 'dddddddd', + inviterName: 'Daenerys Targaryen (daenerys@example.com)' + }) + }) + + it('adds the user to the team', function(done) { + return this.TeamInvitesHandler.acceptInvite( + 'dddddddd', + this.user.id, + () => { + this.SubscriptionUpdater.addUserToGroup + .calledWith(this.subscription._id, this.user.id) + .should.eq(true) + return done() + } + ) + }) + + return it('removes the invite from the subscription', function(done) { + return this.TeamInvitesHandler.acceptInvite( + 'dddddddd', + this.user.id, + () => { + this.Subscription.update + .calledWith( + { _id: new ObjectId('55153a8014829a865bbf700d') }, + { $pull: { teamInvites: { email: 'john.snow@example.com' } } } + ) + .should.eq(true) + return done() + } + ) + }) + }) + + describe('revokeInvite', () => + it('removes the team invite from the subscription', function(done) { + return this.TeamInvitesHandler.revokeInvite( + this.manager.id, + this.subscription, + 'jorah@example.com', + () => { + this.Subscription.update + .calledWith( + { _id: new ObjectId('55153a8014829a865bbf700d') }, + { $pull: { teamInvites: { email: 'jorah@example.com' } } } + ) + .should.eq(true) + + this.Subscription.update + .calledWith( + { _id: new ObjectId('55153a8014829a865bbf700d') }, + { $pull: { invited_emails: 'jorah@example.com' } } + ) + .should.eq(true) + return done() + } + ) + })) + + describe('createTeamInvitesForLegacyInvitedEmail', function(done) { + beforeEach(function() { + this.subscription.invited_emails = [ + 'eddard@example.com', + 'robert@example.com' + ] + this.TeamInvitesHandler.createInvite = sinon.stub().yields(null) + return (this.SubscriptionLocator.getGroupsWithEmailInvite = sinon + .stub() + .yields(null, [this.subscription])) + }) + + return it('sends an invitation email to addresses in the legacy invited_emails field', function(done) { + return this.TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail( + 'eddard@example.com', + (err, invite) => { + expect(err).not.to.exist + + this.TeamInvitesHandler.createInvite + .calledWith( + this.subscription.admin_id, + this.subscription, + 'eddard@example.com' + ) + .should.eq(true) + + this.TeamInvitesHandler.createInvite.callCount.should.eq(1) + + return done() + } + ) + }) + }) + + return describe('validation', function() { + it("doesn't create an invite if the team limit has been reached", function(done) { + this.LimitationsManager.teamHasReachedMemberLimit = sinon + .stub() + .returns(true) + return this.TeamInvitesHandler.createInvite( + this.manager.id, + this.subscription, + 'John.Snow@example.com', + (err, invite) => { + expect(err).to.deep.equal({ limitReached: true }) + return done() + } + ) + }) + + it("doesn't create an invite if the subscription is not in a group plan", function(done) { + this.subscription.groupPlan = false + return this.TeamInvitesHandler.createInvite( + this.manager.id, + this.subscription, + 'John.Snow@example.com', + (err, invite) => { + expect(err).to.deep.equal({ wrongPlan: true }) + return done() + } + ) + }) + + return it("doesn't create an invite if the user is already part of the team", function(done) { + const member = { + id: '1a2b', + _id: '1a2b', + email: 'tyrion@example.com' + } + + this.subscription.member_ids = [member.id] + this.UserGetter.getUserByAnyEmail + .withArgs(member.email) + .yields(null, member) + + return this.TeamInvitesHandler.createInvite( + this.manager.id, + this.subscription, + 'tyrion@example.com', + (err, invite) => { + expect(err).to.deep.equal({ alreadyInTeam: true }) + expect(invite).not.to.exist + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/UserFeaturesUpdaterTests.js b/services/web/test/unit/src/Subscription/UserFeaturesUpdaterTests.js new file mode 100644 index 0000000000..f9e921088a --- /dev/null +++ b/services/web/test/unit/src/Subscription/UserFeaturesUpdaterTests.js @@ -0,0 +1,57 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const should = require('chai').should() +const sinon = require('sinon') +const modulePath = + '../../../../app/src/Features/Subscription/UserFeaturesUpdater' +const { assert } = require('chai') + +describe('UserFeaturesUpdater', function() { + beforeEach(function() { + this.User = { update: sinon.stub().callsArgWith(2) } + return (this.UserFeaturesUpdater = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: this.User + }, + 'logger-sharelatex': { + log() {} + } + } + })) + }) + + return describe('updateFeatures', () => + it('should send the users features', function(done) { + const user_id = '5208dd34438842e2db000005' + this.features = { versioning: true, collaborators: 10 } + return this.UserFeaturesUpdater.updateFeatures( + user_id, + this.features, + (err, features) => { + const update = { + 'features.versioning': true, + 'features.collaborators': 10 + } + this.User.update + .calledWith({ _id: user_id }, update) + .should.equal(true) + features.should.deep.equal(this.features) + return done() + } + ) + })) +}) diff --git a/services/web/test/unit/src/Subscription/V1SusbcriptionManagerTests.js b/services/web/test/unit/src/Subscription/V1SusbcriptionManagerTests.js new file mode 100644 index 0000000000..f8b8e97122 --- /dev/null +++ b/services/web/test/unit/src/Subscription/V1SusbcriptionManagerTests.js @@ -0,0 +1,388 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/Subscription/V1SubscriptionManager' +) +const sinon = require('sinon') +const { expect } = require('chai') + +describe('V1SubscriptionManager', function() { + beforeEach(function() { + this.V1SubscriptionManager = SandboxedModule.require(modulePath, { + requires: { + '../User/UserGetter': (this.UserGetter = {}), + 'logger-sharelatex': { + log: sinon.stub(), + err: sinon.stub(), + warn: sinon.stub() + }, + 'settings-sharelatex': (this.Settings = { + apis: { + v1: { + host: (this.host = 'http://overleaf.example.com') + } + }, + v1GrandfatheredFeaturesUidCutoff: 10, + v1GrandfatheredFeatures: { + github: true, + mendeley: true + } + }), + request: (this.request = sinon.stub()) + } + }) + this.userId = 'abcd' + this.v1UserId = 42 + return (this.user = { + _id: this.userId, + email: 'user@example.com', + overleaf: { + id: this.v1UserId + } + }) + }) + + describe('getPlanCodeFromV1', function() { + beforeEach(function() { + this.responseBody = { + id: 32, + plan_name: 'pro' + } + this.V1SubscriptionManager._v1Request = sinon + .stub() + .yields(null, this.responseBody) + return (this.call = cb => { + return this.V1SubscriptionManager.getPlanCodeFromV1(this.userId, cb) + }) + }) + + return describe('when all goes well', function() { + it('should call _v1Request', function(done) { + return this.call((err, planCode) => { + expect(this.V1SubscriptionManager._v1Request.callCount).to.equal(1) + expect( + this.V1SubscriptionManager._v1Request.calledWith(this.userId) + ).to.equal(true) + return done() + }) + }) + + it('should return the v1 user id', function(done) { + return this.call(function(err, planCode, v1Id) { + expect(v1Id).to.equal(this.v1UserId) + return done() + }) + }) + + it('should produce a plan-code without error', function(done) { + return this.call((err, planCode) => { + expect(err).to.not.exist + expect(planCode).to.equal('v1_pro') + return done() + }) + }) + + return describe('when the plan_name from v1 is null', function() { + beforeEach(function() { + return (this.responseBody.plan_name = null) + }) + + return it('should produce a null plan-code without error', function(done) { + return this.call((err, planCode) => { + expect(err).to.not.exist + expect(planCode).to.equal(null) + return done() + }) + }) + }) + }) + }) + + describe('getGrandfatheredFeaturesForV1User', function() { + describe('when the user ID is greater than the cutoff', () => + it('should return an empty feature set', function(done) { + expect( + this.V1SubscriptionManager.getGrandfatheredFeaturesForV1User(100) + ).to.eql({}) + return done() + })) + + return describe('when the user ID is less than the cutoff', () => + it('should return a feature set with grandfathered properties for github and mendeley', function(done) { + expect( + this.V1SubscriptionManager.getGrandfatheredFeaturesForV1User(1) + ).to.eql({ + github: true, + mendeley: true + }) + return done() + })) + }) + + describe('_v1Request', function() { + beforeEach(function() { + return (this.UserGetter.getUser = sinon.stub().yields(null, this.user)) + }) + + describe('when v1IdForUser produces an error', function() { + beforeEach(function() { + this.V1SubscriptionManager.v1IdForUser = sinon + .stub() + .yields(new Error('woops')) + return (this.call = cb => { + return this.V1SubscriptionManager._v1Request( + this.user_id, + { + url() { + return '/foo' + } + }, + cb + ) + }) + }) + + it('should not call request', function(done) { + return this.call((err, planCode) => { + expect(this.request.callCount).to.equal(0) + return done() + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, planCode) => { + expect(err).to.exist + return done() + }) + }) + }) + + describe('when v1IdForUser does not find a user', function() { + beforeEach(function() { + this.V1SubscriptionManager.v1IdForUser = sinon.stub().yields(null, null) + return (this.call = cb => { + return this.V1SubscriptionManager._v1Request( + this.user_id, + { + url() { + return '/foo' + } + }, + cb + ) + }) + }) + + it('should not call request', function(done) { + return this.call((err, planCode) => { + expect(this.request.callCount).to.equal(0) + return done() + }) + }) + + return it('should not error', function(done) { + return this.call(err => { + expect(err).to.not.exist + return done() + }) + }) + }) + + describe('when the request to v1 fails', function() { + beforeEach(function() { + this.request.yields(new Error('woops')) + return (this.call = cb => { + return this.V1SubscriptionManager._v1Request( + this.user_id, + { + url() { + return '/foo' + } + }, + cb + ) + }) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.exist + return done() + }) + }) + }) + + describe('when the call succeeds', function() { + beforeEach(function() { + this.V1SubscriptionManager.v1IdForUser = sinon + .stub() + .yields(null, this.v1UserId) + this.request.yields(null, { statusCode: 200 }, '{}') + return (this.call = cb => { + return this.V1SubscriptionManager._v1Request( + this.user_id, + { + url() { + return '/foo' + } + }, + cb + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, body, v1Id) => { + expect(err).not.to.exist + return done() + }) + }) + + it('should return the v1 user id', function(done) { + return this.call((err, body, v1Id) => { + expect(v1Id).to.equal(this.v1UserId) + return done() + }) + }) + + return it('should return the http response body', function(done) { + return this.call((err, body, v1Id) => { + expect(body).to.equal('{}') + return done() + }) + }) + }) + + describe('when the call returns an http error status code', function() { + beforeEach(function() { + this.V1SubscriptionManager.v1IdForUser = sinon + .stub() + .yields(null, this.v1UserId) + this.request.yields(null, { statusCode: 500 }, '{}') + return (this.call = cb => { + return this.V1SubscriptionManager._v1Request( + this.user_id, + { + url() { + return '/foo' + } + }, + cb + ) + }) + }) + + return it('should produce an error', function(done) { + return this.call((err, body, v1Id) => { + expect(err).to.exist + return done() + }) + }) + }) + + return describe('when the call returns an http not-found status code', function() { + beforeEach(function() { + this.V1SubscriptionManager.v1IdForUser = sinon + .stub() + .yields(null, this.v1UserId) + this.request.yields(null, { statusCode: 404 }, '{}') + return (this.call = cb => { + return this.V1SubscriptionManager._v1Request( + this.user_id, + { + url() { + return '/foo' + } + }, + cb + ) + }) + }) + + return it('should produce an not-found error', function(done) { + return this.call((err, body, v1Id) => { + expect(err).to.exist + expect(err.name).to.equal('NotFoundError') + return done() + }) + }) + }) + }) + + return describe('v1IdForUser', function() { + beforeEach(function() { + return (this.UserGetter.getUser = sinon.stub().yields(null, this.user)) + }) + + describe('when getUser produces an error', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon.stub().yields(new Error('woops')) + return (this.call = cb => { + return this.V1SubscriptionManager.v1IdForUser(this.user_id, cb) + }) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.exist + return done() + }) + }) + }) + + describe('when getUser does not find a user', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon.stub().yields(null, null) + return (this.call = cb => { + return this.V1SubscriptionManager.v1IdForUser(this.user_id, cb) + }) + }) + + return it('should not error', function(done) { + return this.call((err, user_id) => { + expect(err).to.not.exist + return done() + }) + }) + }) + + return describe('when it works', function() { + beforeEach(function() { + return (this.call = cb => { + return this.V1SubscriptionManager.v1IdForUser(this.user_id, cb) + }) + }) + + it('should not error', function(done) { + return this.call((err, user_id) => { + expect(err).to.not.exist + return done() + }) + }) + + return it('should return the v1 user id', function(done) { + return this.call((err, user_id) => { + expect(user_id).to.eql(42) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/SudoMode/SudoModeControllerTests.js b/services/web/test/unit/src/SudoMode/SudoModeControllerTests.js new file mode 100644 index 0000000000..6a3b694f24 --- /dev/null +++ b/services/web/test/unit/src/SudoMode/SudoModeControllerTests.js @@ -0,0 +1,452 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const should = require('chai').should() +const { expect } = require('chai') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const modulePath = '../../../../app/src/Features/SudoMode/SudoModeController' + +describe('SudoModeController', function() { + beforeEach(function() { + this.user = { + _id: 'abcd', + email: 'user@example.com' + } + this.UserGetter = { getUser: sinon.stub().callsArgWith(2, null, this.user) } + this.SudoModeHandler = { + authenticate: sinon.stub(), + isSudoModeActive: sinon.stub(), + activateSudoMode: sinon.stub() + } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user._id), + _getRediretFromSession: sinon.stub() + } + this.UserGetter = { getUser: sinon.stub() } + return (this.SudoModeController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, + './SudoModeHandler': this.SudoModeHandler, + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../../infrastructure/Mongoose': { + mongo: { + ObjectId() { + return 'some_object_id' + } + } + }, + '../User/UserGetter': this.UserGetter, + 'settings-sharelatex': (this.Settings = {}) + } + })) + }) + + describe('sudoModePrompt', function() { + beforeEach(function() { + this.SudoModeHandler.isSudoModeActive = sinon + .stub() + .callsArgWith(1, null, false) + this.req = { + externalAuthenticationSystemUsed: sinon.stub().returns(false) + } + this.res = { redirect: sinon.stub(), render: sinon.stub() } + return (this.next = sinon.stub()) + }) + + it('should get the logged in user id', function() { + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + this.AuthenticationController.getLoggedInUserId.callCount.should.equal(1) + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should check if sudo-mode is active', function() { + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + this.SudoModeHandler.isSudoModeActive.callCount.should.equal(1) + return this.SudoModeHandler.isSudoModeActive + .calledWith(this.user._id) + .should.equal(true) + }) + + it('should redirect when sudo-mode is active', function() { + this.SudoModeHandler.isSudoModeActive = sinon + .stub() + .callsArgWith(1, null, true) + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + this.res.redirect.callCount.should.equal(1) + return this.res.redirect.calledWith('/project').should.equal(true) + }) + + it('should render the sudo_mode_prompt page when sudo mode is not active', function() { + this.SudoModeHandler.isSudoModeActive = sinon + .stub() + .callsArgWith(1, null, false) + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + this.res.render.callCount.should.equal(1) + return this.res.render + .calledWith('sudo_mode/sudo_mode_prompt') + .should.equal(true) + }) + + describe('when isSudoModeActive produces an error', function() { + beforeEach(function() { + this.SudoModeHandler.isSudoModeActive = sinon + .stub() + .callsArgWith(1, new Error('woops')) + return (this.next = sinon.stub()) + }) + + it('should call next with an error', function() { + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + return it('should not render page', function() { + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + return this.res.render.callCount.should.equal(0) + }) + }) + + return describe('when external auth system is used', function() { + beforeEach(function() { + return (this.req.externalAuthenticationSystemUsed = sinon + .stub() + .returns(true)) + }) + + it('should redirect', function() { + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + this.res.redirect.callCount.should.equal(1) + return this.res.redirect.calledWith('/project').should.equal(true) + }) + + it('should not check if sudo mode is active', function() { + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + return this.SudoModeHandler.isSudoModeActive.callCount.should.equal(0) + }) + + return it('should not render page', function() { + this.SudoModeController.sudoModePrompt(this.req, this.res, this.next) + return this.res.render.callCount.should.equal(0) + }) + }) + }) + + return describe('submitPassword', function() { + beforeEach(function() { + this.AuthenticationController._getRedirectFromSession = sinon + .stub() + .returns('/somewhere') + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user) + this.SudoModeHandler.authenticate = sinon + .stub() + .callsArgWith(2, null, this.user) + this.SudoModeHandler.activateSudoMode = sinon.stub().callsArgWith(1, null) + this.password = 'a_terrible_secret' + this.req = { body: { password: this.password } } + this.res = { json: sinon.stub() } + return (this.next = sinon.stub()) + }) + + return describe('when all goes well', function() { + beforeEach(function() {}) + + it('should get the logged in user id', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.AuthenticationController.getLoggedInUserId.callCount.should.equal( + 1 + ) + return this.AuthenticationController.getLoggedInUserId + .calledWith(this.req) + .should.equal(true) + }) + + it('should get redirect from session', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.AuthenticationController._getRedirectFromSession.callCount.should.equal( + 1 + ) + return this.AuthenticationController._getRedirectFromSession + .calledWith(this.req) + .should.equal(true) + }) + + it('should get the user from storage', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith('some_object_id', { email: 1 }) + .should.equal(true) + }) + + it('should try to authenticate the user with the password', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.SudoModeHandler.authenticate.callCount.should.equal(1) + return this.SudoModeHandler.authenticate + .calledWith(this.user.email, this.password) + .should.equal(true) + }) + + it('should activate sudo mode', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.SudoModeHandler.activateSudoMode.callCount.should.equal(1) + return this.SudoModeHandler.activateSudoMode + .calledWith(this.user._id) + .should.equal(true) + }) + + it('should send back a json response', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.res.json.callCount.should.equal(1) + return this.res.json + .calledWith({ redir: '/somewhere' }) + .should.equal(true) + }) + + it('should not call next', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + describe('when no password is supplied', function() { + beforeEach(function() { + this.req.body.password = '' + return (this.next = sinon.stub()) + }) + + it('should return next with an error', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should not get the user from storage', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.UserGetter.getUser.callCount.should.equal(0) + }) + + it('should not try to authenticate the user with the password', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.authenticate.callCount.should.equal(0) + }) + + it('should not activate sudo mode', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.activateSudoMode.callCount.should.equal(0) + }) + + return it('should not send back a json response', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.res.json.callCount.should.equal(0) + }) + }) + + describe('when getUser produces an error', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, new Error('woops')) + return (this.next = sinon.stub()) + }) + + it('should return next with an error', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should get the user from storage', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith('some_object_id', { email: 1 }) + .should.equal(true) + }) + + it('should not try to authenticate the user with the password', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.authenticate.callCount.should.equal(0) + }) + + it('should not activate sudo mode', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.activateSudoMode.callCount.should.equal(0) + }) + + return it('should not send back a json response', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.res.json.callCount.should.equal(0) + }) + }) + + describe('when getUser does not find a user', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) + return (this.next = sinon.stub()) + }) + + it('should return next with an error', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should get the user from storage', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith('some_object_id', { email: 1 }) + .should.equal(true) + }) + + it('should not try to authenticate the user with the password', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.authenticate.callCount.should.equal(0) + }) + + it('should not activate sudo mode', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.activateSudoMode.callCount.should.equal(0) + }) + + return it('should not send back a json response', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.res.json.callCount.should.equal(0) + }) + }) + + describe('when authentication fails', function() { + beforeEach(function() { + this.SudoModeHandler.authenticate = sinon + .stub() + .callsArgWith(2, null, null) + this.res.json = sinon.stub() + return (this.req.i18n = { translate: sinon.stub() }) + }) + + it('should send back a failure message', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.res.json.callCount.should.equal(1) + expect(this.res.json.lastCall.args[0]).to.have.keys(['message']) + expect(this.res.json.lastCall.args[0].message).to.have.keys([ + 'text', + 'type' + ]) + this.req.i18n.translate.callCount.should.equal(1) + return this.req.i18n.translate.calledWith('invalid_password') + }) + + it('should get the user from storage', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith('some_object_id', { email: 1 }) + .should.equal(true) + }) + + it('should try to authenticate the user with the password', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.SudoModeHandler.authenticate.callCount.should.equal(1) + return this.SudoModeHandler.authenticate + .calledWith(this.user.email, this.password) + .should.equal(true) + }) + + return it('should not activate sudo mode', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.activateSudoMode.callCount.should.equal(0) + }) + }) + + describe('when authentication produces an error', function() { + beforeEach(function() { + this.SudoModeHandler.authenticate = sinon + .stub() + .callsArgWith(2, new Error('woops')) + return (this.next = sinon.stub()) + }) + + it('should return next with an error', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should get the user from storage', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith('some_object_id', { email: 1 }) + .should.equal(true) + }) + + it('should try to authenticate the user with the password', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.SudoModeHandler.authenticate.callCount.should.equal(1) + return this.SudoModeHandler.authenticate + .calledWith(this.user.email, this.password) + .should.equal(true) + }) + + return it('should not activate sudo mode', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + return this.SudoModeHandler.activateSudoMode.callCount.should.equal(0) + }) + }) + + return describe('when sudo mode activation produces an error', function() { + beforeEach(function() { + this.SudoModeHandler.activateSudoMode = sinon + .stub() + .callsArgWith(1, new Error('woops')) + return (this.next = sinon.stub()) + }) + + it('should return next with an error', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should get the user from storage', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.UserGetter.getUser.callCount.should.equal(1) + return this.UserGetter.getUser + .calledWith('some_object_id', { email: 1 }) + .should.equal(true) + }) + + it('should try to authenticate the user with the password', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.SudoModeHandler.authenticate.callCount.should.equal(1) + return this.SudoModeHandler.authenticate + .calledWith(this.user.email, this.password) + .should.equal(true) + }) + + return it('should have tried to activate sudo mode', function() { + this.SudoModeController.submitPassword(this.req, this.res, this.next) + this.SudoModeHandler.activateSudoMode.callCount.should.equal(1) + return this.SudoModeHandler.activateSudoMode + .calledWith(this.user._id) + .should.equal(true) + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/SudoMode/SudoModeHandlerTests.js b/services/web/test/unit/src/SudoMode/SudoModeHandlerTests.js new file mode 100644 index 0000000000..5b500dc192 --- /dev/null +++ b/services/web/test/unit/src/SudoMode/SudoModeHandlerTests.js @@ -0,0 +1,325 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/SudoMode/SudoModeHandler' +) + +describe('SudoModeHandler', function() { + beforeEach(function() { + this.userId = 'some_user_id' + this.email = 'someuser@example.com' + this.user = { + _id: this.userId, + email: this.email + } + this.rclient = { get: sinon.stub(), set: sinon.stub(), del: sinon.stub() } + this.RedisWrapper = { client: () => this.rclient } + return (this.SudoModeHandler = SandboxedModule.require(modulePath, { + requires: { + '../../infrastructure/RedisWrapper': this.RedisWrapper, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }), + '../Authentication/AuthenticationManager': (this.AuthenticationManager = {}), + 'settings-sharelatex': (this.Settings = {}), + '../V1/V1Handler': (this.V1Handler = { authWithV1: sinon.stub() }), + '../User/UserGetter': (this.UserGetter = { getUser: sinon.stub() }) + } + })) + }) + + describe('_buildKey', () => + it('should build a properly formed key', function() { + return expect(this.SudoModeHandler._buildKey('123')).to.equal( + 'SudoMode:{123}' + ) + })) + + describe('activateSudoMode', function() { + beforeEach(function() { + return (this.call = cb => { + return this.SudoModeHandler.activateSudoMode(this.userId, cb) + }) + }) + + describe('when all goes well', function() { + beforeEach(function() { + return (this.rclient.set = sinon.stub().callsArgWith(4, null)) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.equal(null) + return done() + }) + }) + + return it('should set a value in redis', function(done) { + return this.call(err => { + expect(this.rclient.set.callCount).to.equal(1) + expect( + this.rclient.set.calledWith( + 'SudoMode:{some_user_id}', + '1', + 'EX', + 60 * 60 + ) + ).to.equal(true) + return done() + }) + }) + }) + + describe('when user id is not supplied', function() { + beforeEach(function() { + return (this.call = cb => { + return this.SudoModeHandler.activateSudoMode(null, cb) + }) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not set value in redis', function(done) { + return this.call(err => { + expect(this.rclient.set.callCount).to.equal(0) + return done() + }) + }) + }) + + return describe('when rclient.set produces an error', function() { + beforeEach(function() { + return (this.rclient.set = sinon + .stub() + .callsArgWith(4, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + describe('clearSudoMode', function() { + beforeEach(function() { + this.rclient.del = sinon.stub().callsArgWith(1, null) + return (this.call = cb => { + return this.SudoModeHandler.clearSudoMode(this.userId, cb) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should delete key from redis', function(done) { + return this.call(err => { + expect(this.rclient.del.callCount).to.equal(1) + expect(this.rclient.del.calledWith('SudoMode:{some_user_id}')).to.equal( + true + ) + return done() + }) + }) + + describe('when rclient.del produces an error', function() { + beforeEach(function() { + return (this.rclient.del = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + + return describe('when user id is not supplied', function() { + beforeEach(function() { + return (this.call = cb => { + return this.SudoModeHandler.clearSudoMode(null, cb) + }) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not delete value in redis', function(done) { + return this.call(err => { + expect(this.rclient.del.callCount).to.equal(0) + return done() + }) + }) + }) + }) + + describe('authenticate', function() { + beforeEach(function() { + return (this.AuthenticationManager.authenticate = sinon + .stub() + .callsArgWith(2, null, this.user)) + }) + + return it('should call AuthenticationManager.authenticate', function(done) { + return this.SudoModeHandler.authenticate( + this.email, + 'password', + (err, user) => { + expect(err).to.not.exist + expect(user).to.exist + expect(user).to.deep.equal(this.user) + expect(this.AuthenticationManager.authenticate.callCount).to.equal(1) + return done() + } + ) + }) + }) + + return describe('isSudoModeActive', function() { + beforeEach(function() { + return (this.call = cb => { + return this.SudoModeHandler.isSudoModeActive(this.userId, cb) + }) + }) + + describe('when sudo-mode is active for that user', function() { + beforeEach(function() { + return (this.rclient.get = sinon.stub().callsArgWith(1, null, '1')) + }) + + it('should not produce an error', function(done) { + return this.call((err, isActive) => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should get the value from redis', function(done) { + return this.call((err, isActive) => { + expect(this.rclient.get.callCount).to.equal(1) + expect( + this.rclient.get.calledWith('SudoMode:{some_user_id}') + ).to.equal(true) + return done() + }) + }) + + return it('should produce a true result', function(done) { + return this.call((err, isActive) => { + expect(isActive).to.equal(true) + return done() + }) + }) + }) + + describe('when sudo-mode is not active for that user', function() { + beforeEach(function() { + return (this.rclient.get = sinon.stub().callsArgWith(1, null, null)) + }) + + it('should not produce an error', function(done) { + return this.call((err, isActive) => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should get the value from redis', function(done) { + return this.call((err, isActive) => { + expect(this.rclient.get.callCount).to.equal(1) + expect( + this.rclient.get.calledWith('SudoMode:{some_user_id}') + ).to.equal(true) + return done() + }) + }) + + return it('should produce a false result', function(done) { + return this.call((err, isActive) => { + expect(isActive).to.equal(false) + return done() + }) + }) + }) + + describe('when rclient.get produces an error', function() { + beforeEach(function() { + return (this.rclient.get = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.call((err, isActive) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(isActive).to.be.oneOf([null, undefined]) + return done() + }) + }) + }) + + return describe('when user id is not supplied', function() { + beforeEach(function() { + return (this.call = cb => { + return this.SudoModeHandler.isSudoModeActive(null, cb) + }) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not get value in redis', function(done) { + return this.call(err => { + expect(this.rclient.get.callCount).to.equal(0) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/SudoMode/SudoModeMiddlewareTests.js b/services/web/test/unit/src/SudoMode/SudoModeMiddlewareTests.js new file mode 100644 index 0000000000..1fecdcbd28 --- /dev/null +++ b/services/web/test/unit/src/SudoMode/SudoModeMiddlewareTests.js @@ -0,0 +1,226 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/SudoMode/SudoModeMiddleware' +) + +describe('SudoModeMiddleware', function() { + beforeEach(function() { + this.userId = 'some_user_id' + this.SudoModeHandler = { isSudoModeActive: sinon.stub() } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.userId), + setRedirectInSession: sinon.stub() + } + return (this.SudoModeMiddleware = SandboxedModule.require(modulePath, { + requires: { + './SudoModeHandler': this.SudoModeHandler, + '../Authentication/AuthenticationController': this + .AuthenticationController, + 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, + 'settings-sharelatex': (this.Settings = {}) + } + })) + }) + + return describe('protectPage', function() { + beforeEach(function() { + this.externalAuth = false + return (this.call = cb => { + this.req = { + externalAuthenticationSystemUsed: sinon + .stub() + .returns(this.externalAuth) + } + this.res = { redirect: sinon.stub() } + this.next = sinon.stub() + this.SudoModeMiddleware.protectPage(this.req, this.res, this.next) + return cb() + }) + }) + + describe('when sudo mode is active', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId) + return (this.SudoModeHandler.isSudoModeActive = sinon + .stub() + .callsArgWith(1, null, true)) + }) + + it('should get the current user id', function(done) { + return this.call(() => { + this.AuthenticationController.getLoggedInUserId.callCount.should.equal( + 1 + ) + return done() + }) + }) + + it('should check if sudo-mode is active', function(done) { + return this.call(() => { + this.SudoModeHandler.isSudoModeActive.callCount.should.equal(1) + this.SudoModeHandler.isSudoModeActive + .calledWith(this.userId) + .should.equal(true) + return done() + }) + }) + + return it('should call next', function(done) { + return this.call(() => { + this.next.callCount.should.equal(1) + expect(this.next.lastCall.args[0]).to.equal(undefined) + return done() + }) + }) + }) + + describe('when sudo mode is not active', function() { + beforeEach(function() { + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId) + return (this.SudoModeHandler.isSudoModeActive = sinon + .stub() + .callsArgWith(1, null, false)) + }) + + it('should get the current user id', function(done) { + return this.call(() => { + this.AuthenticationController.getLoggedInUserId.callCount.should.equal( + 1 + ) + return done() + }) + }) + + it('should check if sudo-mode is active', function(done) { + return this.call(() => { + this.SudoModeHandler.isSudoModeActive.callCount.should.equal(1) + this.SudoModeHandler.isSudoModeActive + .calledWith(this.userId) + .should.equal(true) + return done() + }) + }) + + it('should set redirect in session', function(done) { + return this.call(() => { + this.AuthenticationController.setRedirectInSession.callCount.should.equal( + 1 + ) + this.AuthenticationController.setRedirectInSession + .calledWith(this.req) + .should.equal(true) + return done() + }) + }) + + return it('should redirect to the password-prompt page', function(done) { + return this.call(() => { + this.res.redirect.callCount.should.equal(1) + this.res.redirect.calledWith('/confirm-password').should.equal(true) + return done() + }) + }) + }) + + describe('when isSudoModeActive produces an error', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId) + return (this.SudoModeHandler.isSudoModeActive = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + it('should get the current user id', function(done) { + return this.call(() => { + this.AuthenticationController.getLoggedInUserId.callCount.should.equal( + 1 + ) + return done() + }) + }) + + it('should check if sudo-mode is active', function(done) { + return this.call(() => { + this.SudoModeHandler.isSudoModeActive.callCount.should.equal(1) + this.SudoModeHandler.isSudoModeActive + .calledWith(this.userId) + .should.equal(true) + return done() + }) + }) + + return it('should call next with an error', function(done) { + return this.call(() => { + this.next.callCount.should.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + }) + + return describe('when external auth is being used', function() { + beforeEach(function() { + this.externalAuth = true + return (this.call = cb => { + this.req = { + externalAuthenticationSystemUsed: sinon + .stub() + .returns(this.externalAuth) + } + this.res = { redirect: sinon.stub() } + this.next = sinon.stub() + this.SudoModeMiddleware.protectPage(this.req, this.res, this.next) + return cb() + }) + }) + + it('should immediately return next with no args', function(done) { + return this.call(() => { + this.next.callCount.should.equal(1) + expect(this.next.lastCall.args[0]).to.not.exist + return done() + }) + }) + + it('should not get the current user id', function(done) { + return this.call(() => { + this.AuthenticationController.getLoggedInUserId.callCount.should.equal( + 0 + ) + return done() + }) + }) + + return it('should not check if sudo-mode is active', function(done) { + return this.call(() => { + this.SudoModeHandler.isSudoModeActive.callCount.should.equal(0) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/SystemMessages/SystemMessageManagerTests.js b/services/web/test/unit/src/SystemMessages/SystemMessageManagerTests.js new file mode 100644 index 0000000000..29a7f90650 --- /dev/null +++ b/services/web/test/unit/src/SystemMessages/SystemMessageManagerTests.js @@ -0,0 +1,91 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/SystemMessages/SystemMessageManager.js' +) + +describe('SystemMessageManager', function() { + beforeEach(function() { + this.SystemMessage = {} + this.SystemMessageManager = SandboxedModule.require(modulePath, { + requires: { + '../../models/SystemMessage': { SystemMessage: this.SystemMessage } + } + }) + return (this.callback = sinon.stub()) + }) + + describe('getMessage', function() { + beforeEach(function() { + this.messages = ['messages-stub'] + return (this.SystemMessage.find = sinon + .stub() + .callsArgWith(1, null, this.messages)) + }) + + describe('when the messages are not cached', function() { + beforeEach(function() { + return this.SystemMessageManager.getMessages(this.callback) + }) + + it('should look the messages up in the database', function() { + return this.SystemMessage.find.calledWith({}).should.equal(true) + }) + + it('should return the messages', function() { + return this.callback.calledWith(null, this.messages).should.equal(true) + }) + + return it('should cache the messages', function() { + return this.SystemMessageManager._cachedMessages.should.equal( + this.messages + ) + }) + }) + + return describe('when the messages are cached', function() { + beforeEach(function() { + this.SystemMessageManager._cachedMessages = this.messages + return this.SystemMessageManager.getMessages(this.callback) + }) + + it('should not look the messages up in the database', function() { + return this.SystemMessage.find.called.should.equal(false) + }) + + return it('should return the messages', function() { + return this.callback.calledWith(null, this.messages).should.equal(true) + }) + }) + }) + + return describe('clearMessages', function() { + beforeEach(function() { + this.SystemMessage.remove = sinon.stub().callsArg(1) + return this.SystemMessageManager.clearMessages(this.callback) + }) + + it('should remove the messages from the database', function() { + return this.SystemMessage.remove.calledWith({}).should.equal(true) + }) + + return it('should return the callback', function() { + return this.callback.called.should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Tags/TagsControllerTests.js b/services/web/test/unit/src/Tags/TagsControllerTests.js new file mode 100644 index 0000000000..d854cf8436 --- /dev/null +++ b/services/web/test/unit/src/Tags/TagsControllerTests.js @@ -0,0 +1,199 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Tags/TagsController.js' +) + +describe('TagsController', function() { + const user_id = '123nd3ijdks' + const project_id = '123njdskj9jlk' + const tag = 'some_class101' + + beforeEach(function() { + this.handler = { + addProjectToTag: sinon.stub().callsArgWith(3), + removeProjectFromTag: sinon.stub().callsArgWith(3), + deleteTag: sinon.stub().callsArg(2), + renameTag: sinon.stub().callsArg(3), + createTag: sinon.stub() + } + this.AuthenticationController = { + getLoggedInUserId: req => { + return req.session.user._id + } + } + this.controller = SandboxedModule.require(modulePath, { + requires: { + './TagsHandler': this.handler, + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../Authentication/AuthenticationController': this + .AuthenticationController + } + }) + this.req = { + params: { + project_id + }, + session: { + user: { + _id: user_id + } + } + } + + this.res = {} + this.res.status = sinon.stub().returns(this.res) + this.res.end = sinon.stub() + return (this.res.json = sinon.stub()) + }) + + describe('getAllTags', () => + it('should ask the handler for all tags', function(done) { + const allTags = [{ name: 'tag', projects: ['123423', '423423'] }] + this.handler.getAllTags = sinon.stub().callsArgWith(1, null, allTags) + return this.controller.getAllTags(this.req, { + json: body => { + body.should.equal(allTags) + this.handler.getAllTags.calledWith(user_id).should.equal(true) + return done() + } + }) + })) + + describe('createTag', function() { + beforeEach(function() { + this.handler.createTag.callsArgWith(2, null, (this.tag = { mock: 'tag' })) + this.req.session.user._id = this.user_id = 'user-id-123' + this.req.body = { name: (this.name = 'tag-name') } + return this.controller.createTag(this.req, this.res) + }) + + it('should create the tag in the backend', function() { + return this.handler.createTag + .calledWith(this.user_id, this.name) + .should.equal(true) + }) + + return it('should return the tag', function() { + return this.res.json.calledWith(this.tag).should.equal(true) + }) + }) + + describe('deleteTag', function() { + beforeEach(function() { + this.req.params.tag_id = this.tag_id = 'tag-id-123' + this.req.session.user._id = this.user_id = 'user-id-123' + return this.controller.deleteTag(this.req, this.res) + }) + + it('should delete the tag in the backend', function() { + return this.handler.deleteTag + .calledWith(this.user_id, this.tag_id) + .should.equal(true) + }) + + return it('should return 204 status code', function() { + this.res.status.calledWith(204).should.equal(true) + return this.res.end.called.should.equal(true) + }) + }) + + describe('renameTag', function() { + beforeEach(function() { + this.req.params.tag_id = this.tag_id = 'tag-id-123' + return (this.req.session.user._id = this.user_id = 'user-id-123') + }) + + describe('with a name', function() { + beforeEach(function() { + this.req.body = { name: (this.name = 'new-name') } + return this.controller.renameTag(this.req, this.res) + }) + + it('should delete the tag in the backend', function() { + return this.handler.renameTag + .calledWith(this.user_id, this.tag_id, this.name) + .should.equal(true) + }) + + return it('should return 204 status code', function() { + this.res.status.calledWith(204).should.equal(true) + return this.res.end.called.should.equal(true) + }) + }) + + return describe('without a name', function() { + beforeEach(function() { + return this.controller.renameTag(this.req, this.res) + }) + + it('should not call the backend', function() { + return this.handler.renameTag.called.should.equal(false) + }) + + return it('should return 400 (bad request) status code', function() { + this.res.status.calledWith(400).should.equal(true) + return this.res.end.called.should.equal(true) + }) + }) + }) + + describe('addProjectToTag', function() { + beforeEach(function() { + this.req.params.tag_id = this.tag_id = 'tag-id-123' + this.req.params.project_id = this.project_id = 'project-id-123' + this.req.session.user._id = this.user_id = 'user-id-123' + return this.controller.addProjectToTag(this.req, this.res) + }) + + it('should add the tag to the project in the backend', function() { + return this.handler.addProjectToTag + .calledWith(this.user_id, this.tag_id, this.project_id) + .should.equal(true) + }) + + return it('should return 204 status code', function() { + this.res.status.calledWith(204).should.equal(true) + return this.res.end.called.should.equal(true) + }) + }) + + return describe('removeProjectFromTag', function() { + beforeEach(function() { + this.req.params.tag_id = this.tag_id = 'tag-id-123' + this.req.params.project_id = this.project_id = 'project-id-123' + this.req.session.user._id = this.user_id = 'user-id-123' + return this.controller.removeProjectFromTag(this.req, this.res) + }) + + it('should remove the tag from the project in the backend', function() { + return this.handler.removeProjectFromTag + .calledWith(this.user_id, this.tag_id, this.project_id) + .should.equal(true) + }) + + return it('should return 204 status code', function() { + this.res.status.calledWith(204).should.equal(true) + return this.res.end.called.should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/Tags/TagsHandlerTests.js b/services/web/test/unit/src/Tags/TagsHandlerTests.js new file mode 100644 index 0000000000..bf1d1005da --- /dev/null +++ b/services/web/test/unit/src/Tags/TagsHandlerTests.js @@ -0,0 +1,473 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const { assert } = require('chai') +require('chai').should() +const sinon = require('sinon') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/Tags/TagsHandler.js' +) +const _ = require('underscore') + +describe('TagsHandler', function() { + const user_id = 'user-id-123' + const tag_id = 'tag-id-123' + const project_id = 'project-id-123' + const tagsUrl = 'tags.sharelatex.testing' + const tag = 'tag_name' + + beforeEach(function() { + this.request = { + post: sinon.stub().callsArgWith(1), + del: sinon.stub().callsArgWith(1), + get: sinon.stub() + } + this.callback = sinon.stub() + return (this.handler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': { + apis: { tags: { url: tagsUrl } } + }, + request: this.request, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + describe('removeProjectFromAllTags', () => + it('should tell the tags api to remove the project_id from all the users tags', function(done) { + return this.handler.removeProjectFromAllTags(user_id, project_id, () => { + this.request.del + .calledWith({ + url: `${tagsUrl}/user/${user_id}/project/${project_id}`, + timeout: 1000 + }) + .should.equal(true) + return done() + }) + })) + + describe('_groupTagsByProject', () => + it('should group the tags by project_id', function(done) { + const rawTags = [ + { name: 'class101', project_ids: ['1234', '51db33e31a55afd212000007'] }, + { name: 'class201', project_ids: ['1234', '51db33e31a55afd212000007'] }, + { + name: 'research group', + project_ids: ['12', '51da65f2e2c39a2f09000100', 'odjaskdas', 'dasdsa'] + }, + { name: 'different', project_ids: ['1234', 'e2c39a2f09000100'] } + ] + + return this.handler._groupTagsByProject(rawTags, function(err, tags) { + _.size(tags).should.equal(7) + return done() + }) + })) + + describe('_requestTags', function() { + it('should return an err and empty array on error', function(done) { + this.request.get.callsArgWith( + 1, + { something: 'wrong' }, + { statusCode: 200 }, + [] + ) + return this.handler._requestTags(user_id, (err, allTags) => { + allTags.length.should.equal(0) + assert.isDefined(err) + return done() + }) + }) + + it('should return an err and empty array on no body', function(done) { + this.request.get.callsArgWith( + 1, + { something: 'wrong' }, + { statusCode: 200 }, + undefined + ) + return this.handler._requestTags(user_id, (err, allTags) => { + allTags.length.should.equal(0) + assert.isDefined(err) + return done() + }) + }) + + it('should return an err and empty array on non 200 response', function(done) { + this.request.get.callsArgWith(1, null, { statusCode: 201 }, []) + return this.handler._requestTags(user_id, (err, allTags) => { + allTags.length.should.equal(0) + assert.isDefined(err) + return done() + }) + }) + + return it('should return an err and empty array on no body and no response', function(done) { + this.request.get.callsArgWith( + 1, + { something: 'wrong' }, + undefined, + undefined + ) + return this.handler._requestTags(user_id, (err, allTags) => { + allTags.length.should.equal(0) + assert.isDefined(err) + return done() + }) + }) + }) + + describe('getAllTags', function() { + it('should get all tags', function(done) { + const stubbedAllTags = [ + { name: 'tag', project_ids: ['123423', '423423'] } + ] + this.request.get.callsArgWith( + 1, + null, + { statusCode: 200 }, + stubbedAllTags + ) + return this.handler.getAllTags(user_id, (err, allTags) => { + stubbedAllTags.should.deep.equal(allTags) + const getOpts = { + url: `${tagsUrl}/user/${user_id}/tag`, + json: true, + timeout: 1000 + } + this.request.get.calledWith(getOpts).should.equal(true) + return done() + }) + }) + + return it('should return empty arrays if there are no tags', function() { + this.request.get.callsArgWith(1, null, { statusCode: 200 }, null) + return this.handler.getAllTags( + user_id, + (err, allTags, projectGroupedTags) => { + allTags.length.should.equal(0) + return _.size(projectGroupedTags).should.equal(0) + } + ) + }) + }) + + describe('createTag', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.createTag( + user_id, + (this.name = 'tag_name'), + this.callback + ) + }) + + it('should send a request to the tag backend', function() { + return this.request.post + .calledWith({ + url: `${tagsUrl}/user/${user_id}/tag`, + json: { + name: this.name + }, + timeout: 1000 + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('deleteTag', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.deleteTag(user_id, tag_id, this.callback) + }) + + it('should send a request to the tag backend', function() { + return this.request.del + .calledWith({ + url: `${tagsUrl}/user/${user_id}/tag/${tag_id}`, + timeout: 1000 + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + return describe('with error', function() { + beforeEach(function() { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.deleteTag(user_id, tag_id, this.callback) + }) + + return it('should call the callback with an Error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + }) + + describe('renameTag', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.renameTag( + user_id, + tag_id, + (this.name = 'new-name'), + this.callback + ) + }) + + it('should send a request to the tag backend', function() { + return this.request.post + .calledWith({ + url: `${tagsUrl}/user/${user_id}/tag/${tag_id}/rename`, + json: { + name: this.name + }, + timeout: 1000 + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + return describe('with error', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.renameTag(user_id, tag_id, 'name', this.callback) + }) + + return it('should call the callback with an Error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + }) + + describe('removeProjectFromTag', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.removeProjectFromTag( + user_id, + tag_id, + project_id, + this.callback + ) + }) + + it('should send a request to the tag backend', function() { + return this.request.del + .calledWith({ + url: `${tagsUrl}/user/${user_id}/tag/${tag_id}/project/${project_id}`, + timeout: 1000 + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + return describe('with error', function() { + beforeEach(function() { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.removeProjectFromTag( + user_id, + tag_id, + project_id, + this.callback + ) + }) + + return it('should call the callback with an Error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + }) + + describe('addProjectToTag', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.addProjectToTag( + user_id, + tag_id, + project_id, + this.callback + ) + }) + + it('should send a request to the tag backend', function() { + return this.request.post + .calledWith({ + url: `${tagsUrl}/user/${user_id}/tag/${tag_id}/project/${project_id}`, + timeout: 1000 + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + return describe('with error', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.addProjectToTag( + user_id, + tag_id, + project_id, + this.callback + ) + }) + + return it('should call the callback with an Error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + }) + + describe('addProjectToTagName', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.addProjectToTagName( + user_id, + tag, + project_id, + this.callback + ) + }) + + it('should send a request to the tag backend', function() { + return this.request.post + .calledWith({ + json: { + name: tag + }, + url: `${tagsUrl}/user/${user_id}/tag/project/${project_id}`, + timeout: 1000 + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + return describe('with error', function() { + beforeEach(function() { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.addProjectToTagName( + user_id, + tag_id, + project_id, + this.callback + ) + }) + + return it('should call the callback with an Error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + }) + + return describe('updateTagUserIds', function() { + describe('successfully', function() { + beforeEach(function() { + this.request.put = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.handler.updateTagUserIds( + 'old-user-id', + 'new-user-id', + this.callback + ) + }) + + it('should send a request to the tag backend', function() { + return this.request.put + .calledWith({ + json: { + user_id: 'new-user-id' + }, + url: `${tagsUrl}/user/old-user-id/tag`, + timeout: 1000 + }) + .should.equal(true) + }) + + return it('should call the callback with no error', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + return describe('with error', function() { + beforeEach(function() { + this.request.put = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.handler.updateTagUserIds( + 'old-user-id', + 'new-user-id', + this.callback + ) + }) + + return it('should call the callback with an Error', function() { + return this.callback.calledWith(new Error()).should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Templates/TemplatesControllerTests.js b/services/web/test/unit/src/Templates/TemplatesControllerTests.js new file mode 100644 index 0000000000..e5cd86d548 --- /dev/null +++ b/services/web/test/unit/src/Templates/TemplatesControllerTests.js @@ -0,0 +1,117 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const chai = require('chai') +const sinon = require('sinon') + +chai.should() +const { expect } = chai + +const modulePath = '../../../../app/src/Features/Templates/TemplatesController' + +describe('TemplatesController', function() { + beforeEach(function() { + this.user_id = 'user-id' + this.TemplatesController = SandboxedModule.require(modulePath, { + requires: { + '../Authentication/AuthenticationController': (this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user_id) + }), + './TemplatesManager': (this.TemplatesManager = { + createProjectFromV1Template: sinon.stub() + }), + 'logger-sharelatex': { + log() {}, + err() {} + } + } + }) + this.next = sinon.stub() + this.req = { + body: { + brandVariationId: 'brand-variation-id', + compiler: 'compiler', + mainFile: 'main-file', + templateId: 'template-id', + templateName: 'template-name', + templateVersionId: 'template-version-id' + }, + session: { + templateData: 'template-data', + user: { + _id: this.user_id + } + } + } + return (this.res = { redirect: sinon.stub() }) + }) + + return describe('createProjectFromV1Template', function() { + describe('on success', function() { + beforeEach(function() { + this.project = { _id: 'project-id' } + this.TemplatesManager.createProjectFromV1Template.yields( + null, + this.project + ) + return this.TemplatesController.createProjectFromV1Template( + this.req, + this.res, + this.next + ) + }) + + it('should call TemplatesManager', function() { + return this.TemplatesManager.createProjectFromV1Template.should.have.been.calledWithMatch( + 'brand-variation-id', + 'compiler', + 'main-file', + 'template-id', + 'template-name', + 'template-version-id', + 'user-id' + ) + }) + + it('should redirect to project', function() { + return this.res.redirect.should.have.been.calledWith( + '/project/project-id' + ) + }) + + return it('should delete session', function() { + return expect(this.req.session.templateData).to.be.undefined + }) + }) + + return describe('on error', function() { + beforeEach(function() { + this.TemplatesManager.createProjectFromV1Template.yields('error') + return this.TemplatesController.createProjectFromV1Template( + this.req, + this.res, + this.next + ) + }) + + it('should call next with error', function() { + return this.next.should.have.been.calledWith('error') + }) + + return it('should not redirect', function() { + return this.res.redirect.called.should.equal(false) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Templates/TemplatesManagerTests.js b/services/web/test/unit/src/Templates/TemplatesManagerTests.js new file mode 100644 index 0000000000..77a1ef377b --- /dev/null +++ b/services/web/test/unit/src/Templates/TemplatesManagerTests.js @@ -0,0 +1,212 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const chai = require('chai') +const sinon = require('sinon') + +const should = require('chai').should() + +const modulePath = '../../../../app/src/Features/Templates/TemplatesManager' + +describe('TemplatesManager', function() { + beforeEach(function() { + this.project_id = 'project-id' + this.brandVariationId = 'brand-variation-id' + this.compiler = 'pdflatex' + this.imageName = 'TL2017' + this.mainFile = 'main.tex' + this.templateId = 'template-id' + this.templateName = 'template name' + this.templateVersionId = 'template-version-id' + this.user_id = 'user-id' + this.dumpPath = `${this.dumpFolder}/${this.uuid}` + this.callback = sinon.stub() + this.request = sinon.stub().returns({ + pipe() {}, + on() {}, + response: { + statusCode: 200 + } + }) + this.fs = { + unlink: sinon.stub(), + createWriteStream: sinon.stub().returns({ on: sinon.stub().yields() }) + } + this.ProjectUploadManager = { + createProjectFromZipArchiveWithName: sinon + .stub() + .callsArgWith(3, null, { _id: this.project_id }) + } + this.dumpFolder = 'dump/path' + this.ProjectOptionsHandler = { + setCompiler: sinon.stub().callsArgWith(2), + setImageName: sinon.stub().callsArgWith(2), + setBrandVariationId: sinon.stub().callsArgWith(2) + } + this.uuid = '1234' + this.ProjectRootDocManager = { + setRootDocFromName: sinon.stub().callsArgWith(2) + } + this.ProjectDetailsHandler = { + getProjectDescription: sinon.stub(), + fixProjectName: sinon.stub().returns(this.templateName) + } + this.Project = { update: sinon.stub().callsArgWith(3, null) } + this.FileWriter = { ensureDumpFolderExists: sinon.stub().callsArg(0) } + this.TemplatesManager = SandboxedModule.require(modulePath, { + requires: { + '../Uploads/ProjectUploadManager': this.ProjectUploadManager, + '../Project/ProjectOptionsHandler': this.ProjectOptionsHandler, + '../Project/ProjectRootDocManager': this.ProjectRootDocManager, + '../Project/ProjectDetailsHandler': this.ProjectDetailsHandler, + '../Authentication/AuthenticationController': (this.AuthenticationController = { + getLoggedInUserId: sinon.stub() + }), + '../../infrastructure/FileWriter': this.FileWriter, + './TemplatesPublisher': this.TemplatesPublisher, + 'logger-sharelatex': { + log() {}, + err() {} + }, + 'settings-sharelatex': { + path: { + dumpFolder: this.dumpFolder + }, + siteUrl: (this.siteUrl = 'http://localhost:3000'), + apis: { + v1: { + url: (this.v1Url = 'http://overleaf.com'), + user: 'sharelatex', + pass: 'password' + } + }, + overleaf: { + host: this.v1Url + } + }, + uuid: { + v4: () => this.uuid + }, + request: this.request, + fs: this.fs, + '../../models/Project': { Project: this.Project } + } + }) + return (this.zipUrl = + '%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex') + }) + + return describe('createProjectFromV1Template', function() { + describe('when all options passed', function() { + beforeEach(function() { + return this.TemplatesManager.createProjectFromV1Template( + this.brandVariationId, + this.compiler, + this.mainFile, + this.templateId, + this.templateName, + this.templateVersionId, + this.user_id, + this.imageName, + this.callback + ) + }) + + it('should fetch zip from v1 based on template id', function() { + return this.request.should.have.been.calledWith( + `${this.v1Url}/api/v1/sharelatex/templates/${this.templateVersionId}` + ) + }) + + it('should save temporary file', function() { + return this.fs.createWriteStream.should.have.been.calledWith( + this.dumpPath + ) + }) + + it('should create project', function() { + return this.ProjectUploadManager.createProjectFromZipArchiveWithName.should.have.been.calledWithMatch( + this.user_id, + this.templateName, + this.dumpPath + ) + }) + + it('should unlink file', function() { + return this.fs.unlink.should.have.been.calledWith(this.dumpPath) + }) + + it('should set project options when passed', function() { + this.ProjectOptionsHandler.setCompiler.should.have.been.calledWithMatch( + this.project_id, + this.compiler + ) + this.ProjectOptionsHandler.setImageName.should.have.been.calledWithMatch( + this.project_id, + this.imageName + ) + this.ProjectRootDocManager.setRootDocFromName.should.have.been.calledWithMatch( + this.project_id, + this.mainFile + ) + return this.ProjectOptionsHandler.setBrandVariationId.should.have.been.calledWithMatch( + this.project_id, + this.brandVariationId + ) + }) + + it('should update project', function() { + return this.Project.update.should.have.been.calledWithMatch( + { _id: this.project_id }, + { + fromV1TemplateId: this.templateId, + fromV1TemplateVersionId: this.templateVersionId + } + ) + }) + + return it('should ensure that the dump folder exists', function() { + return sinon.assert.called(this.FileWriter.ensureDumpFolderExists) + }) + }) + + return describe('when some options not set', function() { + beforeEach(function() { + return this.TemplatesManager.createProjectFromV1Template( + null, + null, + null, + this.templateId, + this.templateName, + this.templateVersionId, + this.user_id, + null, + this.callback + ) + }) + + return it('should not set missing project options', function() { + this.ProjectOptionsHandler.setCompiler.called.should.equal(false) + this.ProjectRootDocManager.setRootDocFromName.called.should.equal(false) + this.ProjectOptionsHandler.setBrandVariationId.called.should.equal( + false + ) + return this.ProjectOptionsHandler.setImageName.should.have.been.calledWithMatch( + this.project_id, + 'wl_texlive:2018.1' + ) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsControllerTests.js b/services/web/test/unit/src/ThirdPartyDataStore/TpdsControllerTests.js new file mode 100644 index 0000000000..815328d8ee --- /dev/null +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsControllerTests.js @@ -0,0 +1,190 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/ThirdPartyDataStore/TpdsController.js' +) + +describe('TpdsController', function() { + beforeEach(function() { + this.TpdsUpdateHandler = {} + this.TpdsController = SandboxedModule.require(modulePath, { + requires: { + './TpdsUpdateHandler': this.TpdsUpdateHandler, + './UpdateMerger': (this.UpdateMerger = {}), + 'logger-sharelatex': { + log() {}, + err() {} + }, + 'metrics-sharelatex': { + inc() {} + } + } + }) + + return (this.user_id = 'dsad29jlkjas') + }) + + describe('getting an update', () => + it('should process the update with the update reciver', function(done) { + const path = '/projectName/here.txt' + const req = { + pause() {}, + params: { 0: path, user_id: this.user_id }, + session: { + destroy() {} + }, + headers: { + 'x-sl-update-source': (this.source = 'dropbox') + } + } + this.TpdsUpdateHandler.newUpdate = sinon.stub().callsArg(5) + const res = { + sendStatus: () => { + this.TpdsUpdateHandler.newUpdate + .calledWith( + this.user_id, + 'projectName', + '/here.txt', + req, + this.source + ) + .should.equal(true) + return done() + } + } + return this.TpdsController.mergeUpdate(req, res) + })) + + describe('getting a delete update', () => + it('should process the delete with the update reciver', function(done) { + const path = '/projectName/here.txt' + const req = { + params: { 0: path, user_id: this.user_id }, + session: { + destroy() {} + }, + headers: { + 'x-sl-update-source': (this.source = 'dropbox') + } + } + this.TpdsUpdateHandler.deleteUpdate = sinon.stub().callsArg(4) + const res = { + sendStatus: () => { + this.TpdsUpdateHandler.deleteUpdate + .calledWith(this.user_id, 'projectName', '/here.txt', this.source) + .should.equal(true) + return done() + } + } + return this.TpdsController.deleteUpdate(req, res) + })) + + describe('parseParams', function() { + it('should take the project name off the start and replace with slash', function() { + const path = 'noSlashHere' + const req = { params: { 0: path, user_id: this.user_id } } + const result = this.TpdsController.parseParams(req) + result.user_id.should.equal(this.user_id) + result.filePath.should.equal('/') + return result.projectName.should.equal(path) + }) + + it('should take the project name off the start and return it with no slashes in', function() { + const path = '/project/file.tex' + const req = { params: { 0: path, user_id: this.user_id } } + const result = this.TpdsController.parseParams(req) + result.user_id.should.equal(this.user_id) + result.filePath.should.equal('/file.tex') + return result.projectName.should.equal('project') + }) + + return it('should take the project name of and return a slash for the file path', function() { + const path = '/project_name' + const req = { params: { 0: path, user_id: this.user_id } } + const result = this.TpdsController.parseParams(req) + result.projectName.should.equal('project_name') + return result.filePath.should.equal('/') + }) + }) + + describe('updateProjectContents', function() { + beforeEach(function() { + this.UpdateMerger.mergeUpdate = sinon.stub().callsArg(5) + this.req = { + params: { + 0: (this.path = 'chapters/main.tex'), + project_id: (this.project_id = 'project-id-123') + }, + session: { + destroy: sinon.stub() + }, + headers: { + 'x-sl-update-source': (this.source = 'github') + } + } + this.res = { sendStatus: sinon.stub() } + + return this.TpdsController.updateProjectContents(this.req, this.res) + }) + + it('should merge the update', function() { + return this.UpdateMerger.mergeUpdate + .calledWith( + null, + this.project_id, + `/${this.path}`, + this.req, + this.source + ) + .should.equal(true) + }) + + return it('should return a success', function() { + return this.res.sendStatus.calledWith(200).should.equal(true) + }) + }) + + return describe('deleteProjectContents', function() { + beforeEach(function() { + this.UpdateMerger.deleteUpdate = sinon.stub().callsArg(4) + this.req = { + params: { + 0: (this.path = 'chapters/main.tex'), + project_id: (this.project_id = 'project-id-123') + }, + session: { + destroy: sinon.stub() + }, + headers: { + 'x-sl-update-source': (this.source = 'github') + } + } + this.res = { sendStatus: sinon.stub() } + + return this.TpdsController.deleteProjectContents(this.req, this.res) + }) + + it('should delete the file', function() { + return this.UpdateMerger.deleteUpdate + .calledWith(null, this.project_id, `/${this.path}`, this.source) + .should.equal(true) + }) + + return it('should return a success', function() { + return this.res.sendStatus.calledWith(200).should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandlerTests.js b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandlerTests.js new file mode 100644 index 0000000000..3040837ab9 --- /dev/null +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandlerTests.js @@ -0,0 +1,258 @@ +/* eslint-disable + camelcase, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const { expect } = require('chai') +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js' +) + +describe('TpdsUpdateHandler', function() { + beforeEach(function() { + this.requestQueuer = {} + this.updateMerger = { + deleteUpdate(user_id, project_id, path, source, cb) { + return cb() + }, + mergeUpdate(user_id, project_id, path, update, source, cb) { + return cb() + } + } + this.editorController = {} + this.project_id = 'dsjajilknaksdn' + this.project = { _id: this.project_id, name: 'projectNameHere' } + this.projectLocator = { + findUsersProjectByName: sinon.stub().callsArgWith(2, null, this.project) + } + this.projectCreationHandler = { + createBlankProject: sinon.stub().callsArgWith(2, null, this.project) + } + this.projectDeleter = { + markAsDeletedByExternalSource: sinon.stub().callsArgWith(1) + } + this.rootDocManager = { setRootDocAutomatically: sinon.stub() } + this.FileTypeManager = { + shouldIgnore: sinon.stub().callsArgWith(1, null, false) + } + this.CooldownManager = { + isProjectOnCooldown: sinon.stub().callsArgWith(1, null, false) + } + this.handler = SandboxedModule.require(modulePath, { + requires: { + './UpdateMerger': this.updateMerger, + './Editor/EditorController': this.editorController, + '../Project/ProjectLocator': this.projectLocator, + '../Project/ProjectCreationHandler': this.projectCreationHandler, + '../Project/ProjectDeleter': this.projectDeleter, + '../Project/ProjectRootDocManager': this.rootDocManager, + '../Uploads/FileTypeManager': this.FileTypeManager, + '../Cooldown/CooldownManager': this.CooldownManager, + 'logger-sharelatex': { + log() {} + } + } + }) + this.user_id = 'dsad29jlkjas' + return (this.source = 'dropbox') + }) + + describe('getting an update', function() { + it('should send the update to the update merger', function(done) { + const path = '/path/here' + const update = {} + this.updateMerger.mergeUpdate = sinon.stub() + this.updateMerger.mergeUpdate + .withArgs(this.user_id, this.project_id, path, update, this.source) + .callsArg(5) + return this.handler.newUpdate( + this.user_id, + this.project.name, + path, + update, + this.source, + () => { + this.projectCreationHandler.createBlankProject.called.should.equal( + false + ) + return done() + } + ) + }) + + it('should create a new project if one does not already exit', function(done) { + this.projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) + const path = '/' + return this.handler.newUpdate( + this.user_id, + this.project.name, + path, + {}, + this.source, + () => { + this.projectCreationHandler.createBlankProject + .calledWith(this.user_id, this.project.name) + .should.equal(true) + return done() + } + ) + }) + + it('should set the root doc automatically if a new project is created', function(done) { + this.projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) + this.handler._rootDocTimeoutLength = 0 + const path = '/' + return this.handler.newUpdate( + this.user_id, + this.project.name, + path, + {}, + this.source, + () => { + return setTimeout(() => { + this.rootDocManager.setRootDocAutomatically + .calledWith(this.project._id) + .should.equal(true) + return done() + }, 1) + } + ) + }) + + it('should not update files that should be ignored', function(done) { + this.FileTypeManager.shouldIgnore = sinon + .stub() + .callsArgWith(1, null, true) + this.projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) + const path = '/.gitignore' + this.updateMerger.mergeUpdate = sinon.stub() + return this.handler.newUpdate( + this.user_id, + this.project.name, + path, + {}, + this.source, + () => { + this.updateMerger.mergeUpdate.called.should.equal(false) + return done() + } + ) + }) + + it('should check if the project is on cooldown', function(done) { + this.CooldownManager.isProjectOnCooldown = sinon + .stub() + .callsArgWith(1, null, false) + this.projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) + const path = '/path/here' + const update = {} + this.updateMerger.mergeUpdate = sinon.stub() + this.updateMerger.mergeUpdate + .withArgs(this.user_id, this.project_id, path, update, this.source) + .callsArg(5) + return this.handler.newUpdate( + this.user_id, + this.project.name, + path, + update, + this.source, + err => { + expect(err).to.be.oneOf([null, undefined]) + this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + this.CooldownManager.isProjectOnCooldown + .calledWith(this.project_id) + .should.equal(true) + this.FileTypeManager.shouldIgnore.callCount.should.equal(1) + this.updateMerger.mergeUpdate.callCount.should.equal(1) + return done() + } + ) + }) + + return it('should return error and not proceed with update if project is on cooldown', function(done) { + this.CooldownManager.isProjectOnCooldown = sinon + .stub() + .callsArgWith(1, null, true) + this.projectLocator.findUsersProjectByName = sinon.stub().callsArgWith(2) + this.FileTypeManager.shouldIgnore = sinon + .stub() + .callsArgWith(1, null, false) + const path = '/path/here' + const update = {} + this.updateMerger.mergeUpdate = sinon.stub() + this.updateMerger.mergeUpdate + .withArgs(this.user_id, this.project_id, path, update, this.source) + .callsArg(5) + return this.handler.newUpdate( + this.user_id, + this.project.name, + path, + update, + this.source, + err => { + expect(err).to.not.be.oneOf([null, undefined]) + expect(err).to.be.instanceof(Error) + this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + this.CooldownManager.isProjectOnCooldown + .calledWith(this.project_id) + .should.equal(true) + this.FileTypeManager.shouldIgnore.callCount.should.equal(0) + this.updateMerger.mergeUpdate.callCount.should.equal(0) + return done() + } + ) + }) + }) + + return describe('getting a delete :', function() { + it('should call deleteEntity in the collaberation manager', function(done) { + const path = '/delete/this' + const update = {} + this.updateMerger.deleteUpdate = sinon.stub().callsArg(4) + + return this.handler.deleteUpdate( + this.user_id, + this.project.name, + path, + this.source, + () => { + this.projectDeleter.markAsDeletedByExternalSource + .calledWith(this.project._id) + .should.equal(false) + this.updateMerger.deleteUpdate + .calledWith(this.user_id, this.project_id, path, this.source) + .should.equal(true) + return done() + } + ) + }) + + return it('should mark the project as deleted by external source if path is a single slash', function(done) { + const path = '/' + return this.handler.deleteUpdate( + this.user_id, + this.project.name, + path, + this.source, + () => { + this.projectDeleter.markAsDeletedByExternalSource + .calledWith(this.project._id) + .should.equal(true) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSenderTests.js b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSenderTests.js new file mode 100644 index 0000000000..36ec58814c --- /dev/null +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateSenderTests.js @@ -0,0 +1,238 @@ +/* eslint-disable + camelcase, + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js' +) +const sinon = require('sinon') +const ath = require('path') +const project_id = 'project_id_here' +const user_id = 'user_id_here' +const read_only_ref_1 = 'read_only_ref_1_id_here' +const collaberator_ref_1 = 'collaberator_ref_1_here' +const project_name = 'project_name_here' + +const thirdPartyDataStoreApiUrl = 'http://third-party-json-store.herokuapp.com' +const httpUsername = 'user' +const httpPass = 'pass' +const siteUrl = 'http://www.localhost:3000' +const httpAuthSiteUrl = `http://${httpUsername}:${httpPass}@www.localhost:3000` +const filestoreUrl = 'filestore.sharelatex.com' + +describe('TpdsUpdateSender', function() { + beforeEach(function() { + this.requestQueuer = function(queue, meth, opts, callback) {} + const project = { owner_ref: user_id } + const member_ids = [collaberator_ref_1, read_only_ref_1, user_id] + this.CollaboratorsHandler = { + getInvitedMemberIds: sinon.stub().yields(null, member_ids) + } + this.ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, project) + } + this.docstoreUrl = 'docstore.sharelatex.env' + this.request = sinon.stub().returns({ pipe() {} }) + this.settings = { + siteUrl, + httpAuthSiteUrl, + apis: { + thirdPartyDataStore: { url: thirdPartyDataStoreApiUrl }, + filestore: { + url: filestoreUrl + }, + docstore: { + pubUrl: this.docstoreUrl + } + } + } + return (this.updateSender = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { log() {} }, + '../Project/ProjectGetter': this.ProjectGetter, + request: this.request, + '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler, + 'metrics-sharelatex': { + inc() {} + } + } + })) + }) + + describe('_enqueue', function() { + it('should not call request if there is no tpdsworker url', function(done) { + return this.updateSender._enqueue(null, null, null, err => { + this.request.called.should.equal(false) + return done() + }) + }) + + return it('should post the message to the tpdsworker', function(done) { + this.settings.apis.tpdsworker = { url: 'www.tpdsworker.env' } + const group = 'myproject' + const method = 'somemethod' + const job = 'do something' + this.request.callsArgWith(1) + return this.updateSender._enqueue(group, method, job, err => { + const args = this.request.args[0][0] + args.json.group.should.equal(group) + args.json.job.should.equal(job) + args.json.method.should.equal(method) + args.uri.should.equal( + 'www.tpdsworker.env/enqueue/web_to_tpds_http_requests' + ) + return done() + }) + }) + }) + + return describe('sending updates', function() { + it('queues a post the file with user and file id', function(done) { + const file_id = '4545345' + const path = '/some/path/here.jpg' + this.updateSender._enqueue = function(uid, method, job, callback) { + uid.should.equal(project_id) + job.method.should.equal('post') + job.streamOrigin.should.equal( + `${filestoreUrl}/project/${project_id}/file/${file_id}` + ) + const expectedUrl = `${thirdPartyDataStoreApiUrl}/user/${user_id}/entity/${encodeURIComponent( + project_name + )}${encodeURIComponent(path)}` + job.uri.should.equal(expectedUrl) + job.headers.sl_all_user_ids.should.eql( + JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id]) + ) + return done() + } + return this.updateSender.addFile( + { project_id, file_id, path, project_name }, + function() {} + ) + }) + + it('post doc with stream origin of docstore', function(done) { + const doc_id = '4545345' + const path = '/some/path/here.tex' + const lines = ['line1', 'line2', 'line3'] + + this.updateSender._enqueue = (uid, method, job, callback) => { + uid.should.equal(project_id) + job.method.should.equal('post') + const expectedUrl = `${thirdPartyDataStoreApiUrl}/user/${user_id}/entity/${encodeURIComponent( + project_name + )}${encodeURIComponent(path)}` + job.uri.should.equal(expectedUrl) + job.streamOrigin.should.equal( + `${this.docstoreUrl}/project/${project_id}/doc/${doc_id}/raw` + ) + job.headers.sl_all_user_ids.should.eql( + JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id]) + ) + return done() + } + return this.updateSender.addDoc({ + project_id, + doc_id, + path, + docLines: lines, + project_name + }) + }) + + it('deleting entity', function(done) { + const path = '/path/here/t.tex' + this.updateSender._enqueue = function(uid, method, job, callback) { + uid.should.equal(project_id) + job.method.should.equal('DELETE') + const expectedUrl = `${thirdPartyDataStoreApiUrl}/user/${user_id}/entity/${encodeURIComponent( + project_name + )}${encodeURIComponent(path)}` + job.headers.sl_all_user_ids.should.eql( + JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id]) + ) + job.uri.should.equal(expectedUrl) + return done() + } + return this.updateSender.deleteEntity({ project_id, path, project_name }) + }) + + it('moving entity', function(done) { + const startPath = 'staring/here/file.tex' + const endPath = 'ending/here/file.tex' + this.updateSender._enqueue = function(uid, method, job, callback) { + uid.should.equal(project_id) + job.method.should.equal('put') + job.uri.should.equal( + `${thirdPartyDataStoreApiUrl}/user/${user_id}/entity` + ) + job.json.startPath.should.equal(`/${project_name}/${startPath}`) + job.json.endPath.should.equal(`/${project_name}/${endPath}`) + job.headers.sl_all_user_ids.should.eql( + JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id]) + ) + return done() + } + return this.updateSender.moveEntity({ + project_id, + startPath, + endPath, + project_name + }) + }) + + it('should be able to rename a project using the move entity func', function(done) { + const oldProjectName = '/oldProjectName/' + const newProjectName = '/newProjectName/' + this.updateSender._enqueue = function(uid, method, job, callback) { + uid.should.equal(project_id) + job.method.should.equal('put') + job.uri.should.equal( + `${thirdPartyDataStoreApiUrl}/user/${user_id}/entity` + ) + job.json.startPath.should.equal(oldProjectName) + job.json.endPath.should.equal(newProjectName) + job.headers.sl_all_user_ids.should.eql( + JSON.stringify([collaberator_ref_1, read_only_ref_1, user_id]) + ) + return done() + } + return this.updateSender.moveEntity({ + project_id, + project_name: oldProjectName, + newProjectName + }) + }) + + return it('pollDropboxForUser', function(done) { + this.updateSender._enqueue = sinon.stub().callsArg(3) + return this.updateSender.pollDropboxForUser(user_id, error => { + this.updateSender._enqueue + .calledWith(`poll-dropbox:${user_id}`, 'standardHttpRequest', { + method: 'POST', + uri: `${thirdPartyDataStoreApiUrl}/user/poll`, + json: { + user_ids: [user_id] + } + }) + .should.equal(true) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/UpdateMergerTests.js b/services/web/test/unit/src/ThirdPartyDataStore/UpdateMergerTests.js new file mode 100644 index 0000000000..1265f0fb1d --- /dev/null +++ b/services/web/test/unit/src/ThirdPartyDataStore/UpdateMergerTests.js @@ -0,0 +1,290 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const Stream = require('stream') +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger.js' +) +const BufferedStream = require('bufferedstream') + +describe('UpdateMerger :', function() { + beforeEach(function() { + this.updateMerger = SandboxedModule.require(modulePath, { + requires: { + fs: (this.fs = { unlink: sinon.stub().callsArgWith(1) }), + 'logger-sharelatex': { + log() {}, + err() {} + }, + '../Editor/EditorController': (this.EditorController = {}), + '../Uploads/FileTypeManager': (this.FileTypeManager = {}), + '../../infrastructure/FileWriter': (this.FileWriter = {}), + '../Project/ProjectEntityHandler': (this.ProjectEntityHandler = {}), + 'settings-sharelatex': { path: { dumpPath: 'dump_here' } } + } + }) + this.project_id = 'project_id_here' + this.user_id = 'mock-user-id' + + this.docPath = this.newDocPath = '/folder/doc.tex' + this.filePath = this.newFilePath = '/folder/file.png' + + this.existingDocPath = '/folder/other.tex' + this.existingFilePath = '/folder/fig1.pdf' + + this.linkedFileData = { provider: 'url' } + + this.existingDocs = [{ path: '/main.tex' }, { path: '/folder/other.tex' }] + this.existingFiles = [{ path: '/figure.pdf' }, { path: '/folder/fig1.pdf' }] + this.ProjectEntityHandler.getAllEntities = sinon + .stub() + .callsArgWith(1, null, this.existingDocs, this.existingFiles) + + this.fsPath = '/tmp/file/path' + this.source = 'dropbox' + this.updateRequest = new BufferedStream() + this.FileWriter.writeStreamToDisk = sinon.stub().yields(null, this.fsPath) + return (this.callback = sinon.stub()) + }) + + describe('mergeUpdate', function() { + describe('doc updates for a new doc', function() { + beforeEach(function() { + this.FileTypeManager.getType = sinon.stub().yields(null, false) + this.updateMerger.p.processDoc = sinon.stub().yields() + return this.updateMerger.mergeUpdate( + this.user_id, + this.project_id, + this.docPath, + this.updateRequest, + this.source, + this.callback + ) + }) + + it('should look at the file contents', function() { + return this.FileTypeManager.getType.called.should.equal(true) + }) + + it('should process update as doc', function() { + return this.updateMerger.p.processDoc + .calledWith( + this.project_id, + this.user_id, + this.fsPath, + this.docPath, + this.source + ) + .should.equal(true) + }) + + return it('removes the temp file from disk', function() { + return this.fs.unlink.calledWith(this.fsPath).should.equal(true) + }) + }) + + describe('file updates for a new file ', function() { + beforeEach(function() { + this.FileTypeManager.getType = sinon.stub().yields(null, true) + this.updateMerger.p.processFile = sinon.stub().yields() + return this.updateMerger.mergeUpdate( + this.user_id, + this.project_id, + this.filePath, + this.updateRequest, + this.source, + this.callback + ) + }) + + it('should look at the file contents', function() { + return this.FileTypeManager.getType.called.should.equal(true) + }) + + it('should process update as file', function() { + return this.updateMerger.p.processFile + .calledWith( + this.project_id, + this.fsPath, + this.filePath, + this.source, + this.user_id + ) + .should.equal(true) + }) + + return it('removes the temp file from disk', function() { + return this.fs.unlink.calledWith(this.fsPath).should.equal(true) + }) + }) + + describe('doc updates for an existing doc', function() { + beforeEach(function() { + this.FileTypeManager.getType = sinon.stub() + this.updateMerger.p.processDoc = sinon.stub().yields() + return this.updateMerger.mergeUpdate( + this.user_id, + this.project_id, + this.existingDocPath, + this.updateRequest, + this.source, + this.callback + ) + }) + + it('should not look at the file contents', function() { + return this.FileTypeManager.getType.called.should.equal(false) + }) + + it('should process update as doc', function() { + return this.updateMerger.p.processDoc + .calledWith( + this.project_id, + this.user_id, + this.fsPath, + this.existingDocPath, + this.source + ) + .should.equal(true) + }) + + return it('removes the temp file from disk', function() { + return this.fs.unlink.calledWith(this.fsPath).should.equal(true) + }) + }) + + return describe('file updates for an existing file', function() { + beforeEach(function() { + this.FileTypeManager.getType = sinon.stub() + this.updateMerger.p.processFile = sinon.stub().yields() + return this.updateMerger.mergeUpdate( + this.user_id, + this.project_id, + this.existingFilePath, + this.updateRequest, + this.source, + this.callback + ) + }) + + it('should not look at the file contents', function() { + return this.FileTypeManager.getType.called.should.equal(false) + }) + + it('should process update as file', function() { + return this.updateMerger.p.processFile + .calledWith( + this.project_id, + this.fsPath, + this.existingFilePath, + this.source, + this.user_id + ) + .should.equal(true) + }) + + return it('removes the temp file from disk', function() { + return this.fs.unlink.calledWith(this.fsPath).should.equal(true) + }) + }) + }) + + describe('deleteUpdate', function() { + beforeEach(function() { + this.EditorController.deleteEntityWithPath = sinon.stub().yields() + return this.updateMerger.deleteUpdate( + this.user_id, + this.project_id, + this.docPath, + this.source, + this.callback + ) + }) + + return it('should delete the entity in the editor controller', function() { + return this.EditorController.deleteEntityWithPath + .calledWith(this.project_id, this.docPath, this.source, this.user_id) + .should.equal(true) + }) + }) + + return describe('private methods', function() { + describe('processDoc', function() { + beforeEach(function() { + this.docLines = + '\\documentclass{article}\n\\usepackage[utf8]{inputenc}\n\n\\title{42}\n\\author{Jane Doe}\n\\date{June 2011}' + this.updateMerger.p.readFileIntoTextArray = sinon + .stub() + .yields(null, this.docLines) + this.EditorController.upsertDocWithPath = sinon.stub().yields() + + return this.updateMerger.p.processDoc( + this.project_id, + this.user_id, + this.fsPath, + this.docPath, + this.source, + this.callback + ) + }) + + it('reads the temp file from disk', function() { + return this.updateMerger.p.readFileIntoTextArray + .calledWith(this.fsPath) + .should.equal(true) + }) + + return it('should upsert the doc in the editor controller', function() { + return this.EditorController.upsertDocWithPath + .calledWith( + this.project_id, + this.docPath, + this.docLines, + this.source, + this.user_id + ) + .should.equal(true) + }) + }) + + return describe('processFile', function() { + beforeEach(function() { + this.EditorController.upsertFileWithPath = sinon.stub().yields() + return this.updateMerger.p.processFile( + this.project_id, + this.fsPath, + this.filePath, + this.source, + this.user_id, + this.callback + ) + }) + + return it('should upsert the file in the editor controller', function() { + return this.EditorController.upsertFileWithPath + .calledWith( + this.project_id, + this.filePath, + this.fsPath, + null, + this.source, + this.user_id + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js new file mode 100644 index 0000000000..9d7f12eff8 --- /dev/null +++ b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js @@ -0,0 +1,1864 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/TokenAccess/TokenAccessController' +) +const { expect } = require('chai') +const { ObjectId } = require('mongojs') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const Errors = require('../../../../app/src/Features/Errors/Errors.js') + +describe('TokenAccessController', function() { + beforeEach(function() { + this.readOnlyToken = 'somereadonlytoken' + this.readAndWriteToken = '42somereadandwritetoken' + this.projectId = ObjectId() + this.ownerId = 'owner' + this.project = { + _id: this.projectId, + publicAccesLevel: 'tokenBased', + tokens: { + readOnly: this.readOnlyToken, + readAndWrite: this.readAndWriteToken + }, + owner_ref: this.ownerId + } + this.userId = ObjectId() + this.TokenAccessController = SandboxedModule.require(modulePath, { + requires: { + '../Project/ProjectController': (this.ProjectController = {}), + '../Authentication/AuthenticationController': (this.AuthenticationController = {}), + './TokenAccessHandler': (this.TokenAccessHandler = { + getV1DocPublishedInfo: sinon.stub().yields(null, { + allow: true + }), + getV1DocInfo: sinon.stub().yields(null, { + exists: true, + exported: false + }) + }), + '../../infrastructure/Features': (this.Features = { + hasFeature: sinon.stub().returns(false) + }), + 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, + 'settings-sharelatex': { + overleaf: { + host: 'http://overleaf.test:5000' + } + }, + '../V1/V1Api': (this.V1Api = { + request: sinon.stub().callsArgWith(1, null, {}, { allow: true }) + }) + } + }) + + return (this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId.toString())) + }) + + describe('readAndWriteToken', function() { + beforeEach(function() {}) + + describe('when all goes well', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.calledWith( + this.userId.toString(), + this.projectId + ) + ).to.equal(true) + return done() + }) + + return it('should pass control to loadEditor', function(done) { + expect(this.req.params.Project_id).to.equal(this.projectId.toString()) + expect(this.ProjectController.loadEditor.callCount).to.equal(1) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(true) + return done() + }) + }) + + describe('when the user is already the owner', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.project.owner_ref = this.userId + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(0) + return done() + }) + + return it('should pass control to loadEditor', function(done) { + expect(this.req.params.Project_id).to.equal(this.projectId.toString()) + expect(this.ProjectController.loadEditor.callCount).to.equal(1) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(true) + return done() + }) + }) + + describe('when there is no user', function() { + beforeEach(function() { + return (this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(null)) + }) + + describe('when anonymous read-write access is enabled', function() { + beforeEach(function() { + this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + this.TokenAccessHandler.grantSessionTokenAccess = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should give the user session token access', function(done) { + expect( + this.TokenAccessHandler.grantSessionTokenAccess.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.grantSessionTokenAccess.calledWith( + this.req, + this.projectId, + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + return it('should pass control to loadEditor', function(done) { + expect(this.req.params.Project_id).to.equal(this.projectId.toString()) + expect(this.ProjectController.loadEditor.callCount).to.equal(1) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(true) + return done() + }) + }) + + return describe('when anonymous read-write access is not enabled', function() { + beforeEach(function() { + this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false + this.req = new MockRequest() + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + this.TokenAccessHandler.grantSessionTokenAccess = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should give the user session token access', function(done) { + expect( + this.TokenAccessHandler.grantSessionTokenAccess.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + it('should set redirect in session', function(done) { + expect( + this.AuthenticationController.setRedirectInSession.callCount + ).to.equal(1) + expect( + this.AuthenticationController.setRedirectInSession.calledWith( + this.req + ) + ).to.equal(true) + return done() + }) + + return it('should redirect to restricted page', function(done) { + expect(this.res.redirect.callCount).to.equal(1) + expect(this.res.redirect.calledWith('/restricted')).to.equal(true) + return done() + }) + }) + }) + + describe('when findProject produces an error', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + return it('should call next with an error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + + describe('when findProject does not find a project', function() { + beforeEach(function() {}) + + return describe('when user is present', function() { + beforeEach(function() { + return (this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId.toString())) + }) + + describe('when project does not exist', function() { + beforeEach(function() { + this.req = new MockRequest() + this.req.url = '/123abc' + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.res.render = sinon.stub() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = '123abc' + return (this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null, false)) + }) + + describe('when project was not exported from v1', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should redirect to v1', function(done) { + expect(this.res.redirect.callCount).to.equal(1) + expect( + this.res.redirect.calledWith( + 302, + '/sign_in_to_v1?return_to=/123abc' + ) + ).to.equal(true) + return done() + }) + }) + + describe('when project was not exported from v1 but forcing import to v2', function() { + beforeEach(function() { + return this.Features.hasFeature.returns(true) + }) + + describe('with project name', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon + .stub() + .yields(null, { + exists: true, + exported: false, + has_owner: true, + name: 'A title', + has_assignment: false, + brand_info: null + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page with name', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: '123abc', + name: 'A title', + hasOwner: true, + hasAssignment: false, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + describe('with project owner', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon + .stub() + .yields(null, { + exists: true, + exported: false, + has_owner: true, + name: 'A title', + has_assignment: false, + brand_info: null + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: '123abc', + hasOwner: true, + name: 'A title', + hasAssignment: false, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + describe('without project owner', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon + .stub() + .yields(null, { + exists: true, + exported: false, + has_owner: false, + name: 'A title', + has_assignment: false, + brand_info: null + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: '123abc', + hasOwner: false, + name: 'A title', + hasAssignment: false, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + describe('with assignment', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon + .stub() + .yields(null, { + exists: true, + exported: false, + has_owner: false, + name: 'A title', + has_assignment: true, + brand_info: null + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: '123abc', + hasOwner: false, + name: 'A title', + hasAssignment: true, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + describe('with brand info', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon + .stub() + .yields(null, { + exists: true, + exported: false, + has_owner: false, + name: 'A title', + has_assignment: false, + brand_info: 'wellcome' + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: '123abc', + hasOwner: false, + name: 'A title', + hasAssignment: false, + brandInfo: 'wellcome' + }) + ).to.equal(true) + return done() + }) + }) + + return describe('with anonymous user', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(null) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render anonymous import status page', function(done) { + expect(this.res.render.callCount).to.equal(1) + expect( + this.res.render.calledWith('project/v2-import', { + loginRedirect: '/123abc' + }) + ).to.equal(true) + return done() + }) + }) + }) + + describe('when project was exported from v1', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: true + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with a not-found error', function(done) { + expect(this.next.callCount).to.equal(1) + return done() + }) + }) + + return describe('when project does not exist on v1', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: false, + exported: false + }) + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with a not-found error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.calledWith(new Errors.NotFoundError())).to.equal( + true + ) + return done() + }) + }) + }) + + describe('when token access is off, but user has higher access anyway', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null, true) + this.TokenAccessHandler.findProjectWithHigherAccess = sinon + .stub() + .callsArgWith(2, null, this.project) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should check if user has higher access to the token project', function(done) { + expect( + this.TokenAccessHandler.findProjectWithHigherAccess.callCount + ).to.equal(1) + return done() + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + it('should not call next with a not-found error', function(done) { + expect(this.next.callCount).to.equal(0) + return done() + }) + + return it('should redirect to the canonical project url', function(done) { + expect(this.res.redirect.callCount).to.equal(1) + expect( + this.res.redirect.calledWith(302, `/project/${this.project._id}`) + ).to.equal(true) + return done() + }) + }) + + return describe('when higher access is not available', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null, true) + this.TokenAccessHandler.findProjectWithHigherAccess = sinon + .stub() + .callsArgWith(2, null, null) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should check if user has higher access to the token project', function(done) { + expect( + this.TokenAccessHandler.findProjectWithHigherAccess.callCount + ).to.equal(1) + return done() + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + return it('should call next with a not-found error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + return describe('when adding user to project produces an error', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, new Error('woops')) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.calledWith( + this.userId.toString(), + this.projectId + ) + ).to.equal(true) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + return it('should call next with an error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + }) + + return describe('readOnlyToken', function() { + beforeEach(function() { + return (this.TokenAccessHandler.checkV1Access = sinon + .stub() + .callsArgWith(1, null, true)) + }) + + describe('when access not allowed by v1 api', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.next = sinon.stub() + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.getV1DocPublishedInfo = sinon + .stub() + .yields(null, { + allow: false, + published_path: 'doc-url' + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should redirect to doc-url', function() { + return expect(this.res.redirect.calledWith('doc-url')).to.equal(true) + }) + }) + + describe('with a user', function() { + beforeEach(function() { + return (this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId.toString())) + }) + + describe('when all goes well', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_only_token'] = this.readOnlyToken + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadOnlyUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.calledWith( + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should add the user to the project with read-only access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.calledWith( + this.userId.toString(), + this.projectId + ) + ).to.equal(true) + return done() + }) + + return it('should pass control to loadEditor', function(done) { + expect(this.req.params.Project_id).to.equal(this.projectId.toString()) + expect(this.ProjectController.loadEditor.callCount).to.equal(1) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(true) + return done() + }) + }) + + describe('when the user is already the owner', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_only_token'] = this.readOnlyToken + this.project.owner_ref = this.userId + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadOnlyUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.calledWith( + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should not add the user to the project with read-only access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(0) + return done() + }) + + return it('should pass control to loadEditor', function(done) { + expect(this.req.params.Project_id).to.equal(this.projectId.toString()) + expect(this.ProjectController.loadEditor.callCount).to.equal(1) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(true) + return done() + }) + }) + + return describe('when findProject produces an error', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_only_token'] = this.readOnlyToken + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.TokenAccessHandler.addReadOnlyUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.calledWith( + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should not add the user to the project with read-only access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + return it('should call next with an error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + }) + + describe('when findProject does not find a project', function() { + describe('when project does not exist', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.next = sinon.stub() + this.req.params['read_only_token'] = 'abcd' + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, null, false) + this.TokenAccessHandler.checkV1ProjectExported = sinon + .stub() + .callsArgWith(1, null, false) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should redirect to v1', function(done) { + expect(this.res.redirect.callCount).to.equal(1) + expect( + this.res.redirect.calledWith( + 302, + '/sign_in_to_v1?return_to=/read/abcd' + ) + ).to.equal(true) + return done() + }) + }) + + describe('when project was not exported from v1 but forcing import to v2', function() { + beforeEach(function() { + this.Features.hasFeature.returns(true) + this.req = new MockRequest() + this.res = new MockResponse() + this.res.render = sinon.stub() + this.next = sinon.stub() + this.req.params['read_only_token'] = 'abcd' + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, null, false)) + }) + + describe('with project name', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false, + has_owner: true, + name: 'A title', + has_assignment: false, + brand_info: null + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page with name', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: 'abcd', + name: 'A title', + hasOwner: true, + hasAssignment: false, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + describe('with project owner', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false, + has_owner: true, + name: 'A title', + has_assignment: false, + brand_info: null + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: 'abcd', + hasOwner: true, + name: 'A title', + hasAssignment: false, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + describe('without project owner', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false, + has_owner: false, + name: 'A title', + has_assignment: false, + brand_info: null + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: 'abcd', + hasOwner: false, + name: 'A title', + hasAssignment: false, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + describe('with assignment', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false, + has_owner: false, + name: 'A title', + has_assignment: true, + brand_info: null + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: 'abcd', + hasOwner: false, + name: 'A title', + hasAssignment: true, + brandInfo: null + }) + ).to.equal(true) + return done() + }) + }) + + return describe('with brand info', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false, + has_owner: false, + name: 'A title', + has_assignment: false, + brand_info: 'f1000' + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render v2-import page', function(done) { + expect( + this.res.render.calledWith('project/v2-import', { + projectId: 'abcd', + hasOwner: false, + name: 'A title', + hasAssignment: false, + brandInfo: 'f1000' + }) + ).to.equal(true) + return done() + }) + }) + }) + + describe('when project was exported from v1', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.next = sinon.stub() + this.req.params['read_only_token'] = 'abcd' + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, null, false) + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + allow: true, + exists: true, + exported: true + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with a not-found error', function(done) { + expect(this.next.callCount).to.equal(1) + return done() + }) + }) + + describe('when token access is off, but user has higher access anyway', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null, true) + this.TokenAccessHandler.findProjectWithHigherAccess = sinon + .stub() + .callsArgWith(2, null, this.project) + this.TokenAccessHandler.addReadAndWriteUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should check if user has higher access to the token project', function(done) { + expect( + this.TokenAccessHandler.findProjectWithHigherAccess.callCount + ).to.equal(1) + return done() + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadAndWriteUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + it('should not call next with a not-found error', function(done) { + expect(this.next.callCount).to.equal(0) + return done() + }) + + return it('should redirect to the canonical project url', function(done) { + expect(this.res.redirect.callCount).to.equal(1) + expect( + this.res.redirect.calledWith(302, `/project/${this.project._id}`) + ).to.equal(true) + return done() + }) + }) + + return describe('when higher access is not available', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_and_write_token'] = this.readAndWriteToken + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null, true) + this.TokenAccessHandler.findProjectWithHigherAccess = sinon + .stub() + .callsArgWith(2, null, null) + this.TokenAccessHandler.addReadOnlyUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readAndWriteToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.calledWith( + this.readAndWriteToken + ) + ).to.equal(true) + return done() + }) + + it('should check if user has higher access to the token project', function(done) { + expect( + this.TokenAccessHandler.findProjectWithHigherAccess.callCount + ).to.equal(1) + return done() + }) + + it('should not add the user to the project with read-write access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + return it('should call next with a not-found error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + }) + + describe('when adding user to project produces an error', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_only_token'] = this.readOnlyToken + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadOnlyUserToProject = sinon + .stub() + .callsArgWith(2, new Error('woops')) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.calledWith( + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should add the user to the project with read-only access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.calledWith( + this.userId.toString(), + this.projectId + ) + ).to.equal(true) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + return it('should call next with an error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + + return describe('anonymous', function() { + beforeEach(function() { + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(null) + return (this.TokenAccessHandler.grantSessionTokenAccess = sinon.stub()) + }) + + describe('when all goes well', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_only_token'] = this.readOnlyToken + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, this.project, true) + this.TokenAccessHandler.addReadOnlyUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.calledWith( + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should give the user session read-only access', function(done) { + expect( + this.TokenAccessHandler.grantSessionTokenAccess.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.grantSessionTokenAccess.calledWith( + this.req, + this.projectId, + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should not add the user to the project with read-only access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(0) + return done() + }) + + return it('should pass control to loadEditor', function(done) { + expect(this.req.params.Project_id).to.equal(this.projectId.toString()) + expect(this.req._anonymousAccessToken).to.equal(this.readOnlyToken) + expect(this.ProjectController.loadEditor.callCount).to.equal(1) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(true) + return done() + }) + }) + + describe('when findProject produces an error', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.next = sinon.stub() + this.req.params['read_only_token'] = this.readOnlyToken + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.TokenAccessHandler.addReadOnlyUserToProject = sinon + .stub() + .callsArgWith(2, null) + this.ProjectController.loadEditor = sinon.stub() + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.calledWith( + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should not give the user session read-only access', function(done) { + expect( + this.TokenAccessHandler.grantSessionTokenAccess.callCount + ).to.equal(0) + return done() + }) + + it('should not add the user to the project with read-only access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(0) + return done() + }) + + it('should not pass control to loadEditor', function(done) { + expect(this.ProjectController.loadEditor.callCount).to.equal(0) + expect( + this.ProjectController.loadEditor.calledWith( + this.req, + this.res, + this.next + ) + ).to.equal(false) + return done() + }) + + return it('should call next with an error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + return done() + }) + }) + + return describe('when findProject does not find a project', function() { + beforeEach(function() { + this.req = new MockRequest() + this.res = new MockResponse() + this.res.redirect = sinon.stub() + this.next = sinon.stub() + this.req.params['read_only_token'] = this.readOnlyToken + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.userId.toString()) + this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, false) + return (this.TokenAccessHandler.addReadOnlyUserToProject = sinon.stub()) + }) + + describe('when project does not exist', function() { + beforeEach(function() { + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + it('should try to find a project with this token', function(done) { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.calledWith( + this.readOnlyToken + ) + ).to.equal(true) + return done() + }) + + it('should not give the user session read-only access', function(done) { + expect( + this.TokenAccessHandler.grantSessionTokenAccess.callCount + ).to.equal(0) + return done() + }) + + return it('should not add the user to the project with read-only access', function(done) { + expect( + this.TokenAccessHandler.addReadOnlyUserToProject.callCount + ).to.equal(0) + return done() + }) + }) + + describe('when project was exported to v2', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: true + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with not found error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.calledWith(new Errors.NotFoundError())).to.equal( + true + ) + return done() + }) + }) + + describe('when project was not exported to v2', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should redirect to v1', function(done) { + expect(this.res.redirect.callCount).to.equal(1) + expect( + this.res.redirect.calledWith( + 302, + `/sign_in_to_v1?return_to=/read/${this.readOnlyToken}` + ) + ).to.equal(true) + return done() + }) + }) + + describe('when project does not exist on v1', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: false, + exported: false + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should call next with not found error', function(done) { + expect(this.next.callCount).to.equal(1) + expect(this.next.calledWith(new Errors.NotFoundError())).to.equal( + true + ) + return done() + }) + }) + + return describe('anonymous user', function() { + beforeEach(function() { + return (this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(null)) + }) + + describe('when project was not exported to v2', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon.stub().yields(null, { + exists: true, + exported: false + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should redirect to v1', function(done) { + expect(this.res.redirect.callCount).to.equal(1) + expect( + this.res.redirect.calledWith( + 302, + `/sign_in_to_v1?return_to=/read/${this.readOnlyToken}` + ) + ).to.equal(true) + return done() + }) + }) + + return describe('force-import-to-v2 flag is on', function() { + beforeEach(function() { + this.res.render = sinon.stub() + return this.Features.hasFeature.returns(true) + }) + + return describe('when project was not exported to v2', function() { + beforeEach(function() { + this.TokenAccessHandler.getV1DocInfo = sinon + .stub() + .yields(null, { + exists: true, + exported: false + }) + return this.TokenAccessController.readOnlyToken( + this.req, + this.res, + this.next + ) + }) + + return it('should render anonymous import status page', function(done) { + expect(this.res.render.callCount).to.equal(1) + expect( + this.res.render.calledWith('project/v2-import', { + loginRedirect: `/read/${this.readOnlyToken}` + }) + ).to.equal(true) + return done() + }) + }) + }) + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js b/services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js new file mode 100644 index 0000000000..8c40f55902 --- /dev/null +++ b/services/web/test/unit/src/TokenAccess/TokenAccessHandlerTests.js @@ -0,0 +1,1100 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/TokenAccess/TokenAccessHandler' +) +const { expect } = require('chai') +const { ObjectId } = require('mongojs') + +describe('TokenAccessHandler', function() { + beforeEach(function() { + this.token = 'sometokenthing' + this.projectId = ObjectId() + this.project = { + _id: this.projectId, + publicAccesLevel: 'tokenBased' + } + this.userId = ObjectId() + this.req = {} + return (this.TokenAccessHandler = SandboxedModule.require(modulePath, { + requires: { + '../../models/Project': { Project: (this.Project = {}) }, + 'settings-sharelatex': (this.settings = {}), + '../Collaborators/CollaboratorsHandler': (this.CollaboratorsHandler = {}), + '../User/UserGetter': (this.UserGetter = {}), + '../V1/V1Api': (this.V1Api = { + request: sinon.stub() + }) + } + })) + }) + + describe('findProjectWithReadOnlyToken', function() { + beforeEach(function() { + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, null, this.project)) + }) + + it('should call Project.findOne', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + (err, project) => { + expect(this.Project.findOne.callCount).to.equal(1) + expect( + this.Project.findOne.calledWith({ + 'tokens.readOnly': this.token + }) + ).to.equal(true) + return done() + } + ) + }) + + it('should produce a project object with no error', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + (err, project) => { + expect(err).to.not.exist + expect(project).to.exist + expect(project).to.deep.equal(this.project) + return done() + } + ) + }) + + it('should return projectExists flag as true', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + function(err, project, projectExists) { + expect(projectExists).to.equal(true) + return done() + } + ) + }) + + describe('when Project.findOne produces an error', function() { + beforeEach(function() { + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + (err, project) => { + expect(err).to.exist + expect(project).to.not.exist + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + }) + + describe('when project does not have tokenBased access level', function() { + beforeEach(function() { + this.project.publicAccesLevel = 'private' + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, null, this.project, true)) + }) + + it('should not return a project', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + function(err, project) { + expect(err).to.not.exist + expect(project).to.not.exist + return done() + } + ) + }) + + return it('should return projectExists flag as true', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + function(err, project, projectExists) { + expect(projectExists).to.equal(true) + return done() + } + ) + }) + }) + + return describe('when project does not exist', function() { + beforeEach(function() { + return (this.Project.findOne = sinon.stub().callsArgWith(2, null, null)) + }) + + it('should not return a project', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + function(err, project) { + expect(err).to.not.exist + expect(project).to.not.exist + return done() + } + ) + }) + + return it('should return projectExists flag as false', function(done) { + return this.TokenAccessHandler.findProjectWithReadOnlyToken( + this.token, + function(err, project, projectExists) { + expect(projectExists).to.equal(false) + return done() + } + ) + }) + }) + }) + + describe('findProjectWithReadAndWriteToken', function() { + beforeEach(function() { + this.token = '1234bcdf' + this.tokenPrefix = '1234' + this.project.tokens = { + readOnly: 'atntntn', + readAndWrite: this.token, + readAndWritePrefix: this.tokenPrefix + } + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, null, this.project)) + }) + + it('should call Project.findOne', function(done) { + return this.TokenAccessHandler.findProjectWithReadAndWriteToken( + this.token, + (err, project) => { + expect(this.Project.findOne.callCount).to.equal(1) + expect( + this.Project.findOne.calledWith({ + 'tokens.readAndWritePrefix': this.tokenPrefix + }) + ).to.equal(true) + return done() + } + ) + }) + + it('should produce a project object with no error', function(done) { + return this.TokenAccessHandler.findProjectWithReadAndWriteToken( + this.token, + (err, project) => { + expect(err).to.not.exist + expect(project).to.exist + expect(project).to.deep.equal(this.project) + return done() + } + ) + }) + + it('should return projectExists flag as true', function(done) { + return this.TokenAccessHandler.findProjectWithReadAndWriteToken( + this.token, + function(err, project, projectExists) { + expect(projectExists).to.equal(true) + return done() + } + ) + }) + + describe('when Project.findOne produces an error', function() { + beforeEach(function() { + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.TokenAccessHandler.findProjectWithReadAndWriteToken( + this.token, + (err, project) => { + expect(err).to.exist + expect(project).to.not.exist + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + }) + + describe('when project does not have tokenBased access level', function() { + beforeEach(function() { + this.project.publicAccesLevel = 'private' + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, null, this.project, true)) + }) + + it('should not return a project', function(done) { + return this.TokenAccessHandler.findProjectWithReadAndWriteToken( + this.token, + function(err, project) { + expect(err).to.not.exist + expect(project).to.not.exist + return done() + } + ) + }) + + return it('should return projectExists flag as true', function(done) { + return this.TokenAccessHandler.findProjectWithReadAndWriteToken( + this.token, + function(err, project, projectExists) { + expect(projectExists).to.equal(true) + return done() + } + ) + }) + }) + + return describe('when the tokens have different lengths', function() { + beforeEach(function() { + this.project.tokens = { + readOnly: 'atntntn', + readAndWrite: this.token + 'some-other-characters', + readAndWritePrefix: this.tokenPrefix + } + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, null, this.project)) + }) + + return it('should not return a project', function(done) { + return this.TokenAccessHandler.findProjectWithReadAndWriteToken( + this.token, + function(err, project) { + expect(err).to.not.exist + expect(project).to.not.exist + return done() + } + ) + }) + }) + }) + + describe('findProjectWithHigherAccess', function() { + describe('when user does have higher access', function() { + beforeEach(function() { + this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) + return (this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + .stub() + .callsArgWith(2, null, true)) + }) + + it('should call Project.findOne', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect(this.Project.findOne.callCount).to.equal(1) + expect( + this.Project.findOne.calledWith({ + 'tokens.readOnly': this.token + }) + ).to.equal(true) + return done() + } + ) + }) + + it('should call isUserInvitedMemberOfProject', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect( + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount + ).to.equal(1) + expect( + this.CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith( + this.userId, + this.project._id + ) + ).to.equal(true) + return done() + } + ) + }) + + return it('should produce a project object', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect(err).to.not.exist + expect(project).to.exist + expect(project).to.deep.equal(this.project) + return done() + } + ) + }) + }) + + describe('when user does not have higher access', function() { + beforeEach(function() { + this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) + return (this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + .stub() + .callsArgWith(2, null, false)) + }) + + it('should call Project.findOne', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect(this.Project.findOne.callCount).to.equal(1) + expect( + this.Project.findOne.calledWith({ + 'tokens.readOnly': this.token + }) + ).to.equal(true) + return done() + } + ) + }) + + it('should call isUserInvitedMemberOfProject', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect( + this.CollaboratorsHandler.isUserInvitedMemberOfProject.callCount + ).to.equal(1) + expect( + this.CollaboratorsHandler.isUserInvitedMemberOfProject.calledWith( + this.userId, + this.project._id + ) + ).to.equal(true) + return done() + } + ) + }) + + return it('should not produce a project', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect(err).to.not.exist + expect(project).to.not.exist + return done() + } + ) + }) + }) + + describe('when Project.findOne produces an error', function() { + beforeEach(function() { + return (this.Project.findOne = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect(err).to.exist + expect(project).to.not.exist + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + }) + + return describe('when isUserInvitedMemberOfProject produces an error', function() { + beforeEach(function() { + this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project) + return (this.CollaboratorsHandler.isUserInvitedMemberOfProject = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.TokenAccessHandler.findProjectWithHigherAccess( + this.token, + this.userId, + (err, project) => { + expect(err).to.exist + expect(project).to.not.exist + expect(err).to.be.instanceof(Error) + return done() + } + ) + }) + }) + }) + + describe('addReadOnlyUserToProject', function() { + beforeEach(function() { + return (this.Project.update = sinon.stub().callsArgWith(2, null)) + }) + + it('should call Project.update', function(done) { + return this.TokenAccessHandler.addReadOnlyUserToProject( + this.userId, + this.projectId, + err => { + expect(this.Project.update.callCount).to.equal(1) + expect( + this.Project.update.calledWith({ + _id: this.projectId + }) + ).to.equal(true) + expect( + this.Project.update.lastCall.args[1]['$addToSet'] + ).to.have.keys('tokenAccessReadOnly_refs') + return done() + } + ) + }) + + it('should not produce an error', function(done) { + return this.TokenAccessHandler.addReadOnlyUserToProject( + this.userId, + this.projectId, + err => { + expect(err).to.not.exist + return done() + } + ) + }) + + return describe('when Project.update produces an error', function() { + beforeEach(function() { + return (this.Project.update = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.TokenAccessHandler.addReadOnlyUserToProject( + this.userId, + this.projectId, + err => { + expect(err).to.exist + return done() + } + ) + }) + }) + }) + + describe('addReadAndWriteUserToProject', function() { + beforeEach(function() { + return (this.Project.update = sinon.stub().callsArgWith(2, null)) + }) + + it('should call Project.update', function(done) { + return this.TokenAccessHandler.addReadAndWriteUserToProject( + this.userId, + this.projectId, + err => { + expect(this.Project.update.callCount).to.equal(1) + expect( + this.Project.update.calledWith({ + _id: this.projectId + }) + ).to.equal(true) + expect( + this.Project.update.lastCall.args[1]['$addToSet'] + ).to.have.keys('tokenAccessReadAndWrite_refs') + return done() + } + ) + }) + + it('should not produce an error', function(done) { + return this.TokenAccessHandler.addReadAndWriteUserToProject( + this.userId, + this.projectId, + err => { + expect(err).to.not.exist + return done() + } + ) + }) + + return describe('when Project.update produces an error', function() { + beforeEach(function() { + return (this.Project.update = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.TokenAccessHandler.addReadAndWriteUserToProject( + this.userId, + this.projectId, + err => { + expect(err).to.exist + return done() + } + ) + }) + }) + }) + + describe('grantSessionTokenAccess', function() { + beforeEach(function() { + return (this.req = { session: {}, headers: {} }) + }) + + return it('should add the token to the session', function(done) { + this.TokenAccessHandler.grantSessionTokenAccess( + this.req, + this.projectId, + this.token + ) + expect( + this.req.session.anonTokenAccess[this.projectId.toString()] + ).to.equal(this.token) + return done() + }) + }) + + describe('isValidToken', function() { + describe('when a read-only project is found', function() { + beforeEach(function() { + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null) + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, this.project)) + }) + + it('should try to find projects with both kinds of token', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + return done() + } + ) + }) + + return it('should allow read-only access', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, rw, ro) => { + expect(err).to.not.exist + expect(rw).to.equal(false) + expect(ro).to.equal(true) + return done() + } + ) + }) + }) + + describe('when a read-and-write project is found', function() { + beforeEach(function() { + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project) + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, null)) + }) + + it('should try to find projects with both kinds of token', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + return done() + } + ) + }) + + return it('should allow read-and-write access', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, rw, ro) => { + expect(err).to.not.exist + expect(rw).to.equal(true) + expect(ro).to.equal(false) + return done() + } + ) + }) + }) + + describe('when no project is found', function() { + beforeEach(function() { + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null) + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, null)) + }) + + it('should try to find projects with both kinds of token', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + return done() + } + ) + }) + + return it('should not allow any access', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, rw, ro) => { + expect(err).to.not.exist + expect(rw).to.equal(false) + expect(ro).to.equal(false) + return done() + } + ) + }) + }) + + describe('when findProject produces an error', function() { + beforeEach(function() { + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null) + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + it('should try to find projects with both kinds of token', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + return done() + } + ) + }) + + return it('should produce an error and not allow access', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, rw, ro) => { + expect(err).to.exist + expect(err).to.be.instanceof(Error) + expect(rw).to.equal(undefined) + expect(ro).to.equal(undefined) + return done() + } + ) + }) + }) + + describe('when project is not set to token-based access', function() { + beforeEach(function() { + return (this.project.publicAccesLevel = 'private') + }) + + describe('for read-and-write project', function() { + beforeEach(function() { + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project) + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, null)) + }) + + it('should try to find projects with both kinds of token', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken + .callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + return done() + } + ) + }) + + return it('should not allow any access', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, rw, ro) => { + expect(err).to.not.exist + expect(rw).to.equal(false) + expect(ro).to.equal(false) + return done() + } + ) + }) + }) + + return describe('for read-only project', function() { + beforeEach(function() { + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, null) + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, this.project)) + }) + + it('should try to find projects with both kinds of token', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken + .callCount + ).to.equal(1) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(1) + return done() + } + ) + }) + + return it('should not allow any access', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + this.token, + (err, rw, ro) => { + expect(err).to.not.exist + expect(rw).to.equal(false) + expect(ro).to.equal(false) + return done() + } + ) + }) + }) + }) + + return describe('with nothing', function() { + beforeEach(function() { + this.TokenAccessHandler.findProjectWithReadAndWriteToken = sinon + .stub() + .callsArgWith(1, null, this.project) + return (this.TokenAccessHandler.findProjectWithReadOnlyToken = sinon + .stub() + .callsArgWith(1, null, null)) + }) + + it('should not call findProjectWithReadOnlyToken', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + null, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(0) + return done() + } + ) + }) + + it('should try to find projects with both kinds of token', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + null, + (err, allowed) => { + expect( + this.TokenAccessHandler.findProjectWithReadAndWriteToken.callCount + ).to.equal(0) + expect( + this.TokenAccessHandler.findProjectWithReadOnlyToken.callCount + ).to.equal(0) + return done() + } + ) + }) + + return it('should not allow any access', function(done) { + return this.TokenAccessHandler.isValidToken( + this.projectId, + null, + (err, rw, ro) => { + expect(err).to.not.exist + expect(rw).to.equal(false) + expect(ro).to.equal(false) + return done() + } + ) + }) + }) + }) + + describe('protectTokens', function() { + beforeEach(function() { + return (this.project = { + tokens: { + readAndWrite: 'rw', + readOnly: 'ro', + readAndWritePrefix: 'pre' + } + }) + }) + + it('should hide write token from read-only user', function() { + this.TokenAccessHandler.protectTokens(this.project, 'readOnly') + expect(this.project.tokens.readAndWrite).to.equal('') + expect(this.project.tokens.readAndWritePrefix).to.equal('') + return expect(this.project.tokens.readOnly).to.equal('ro') + }) + + it('should hide read token from read-write user', function() { + this.TokenAccessHandler.protectTokens(this.project, 'readAndWrite') + expect(this.project.tokens.readAndWrite).to.equal('rw') + return expect(this.project.tokens.readOnly).to.equal('') + }) + + return it('should leave tokens in place for owner', function() { + this.TokenAccessHandler.protectTokens(this.project, 'owner') + expect(this.project.tokens.readAndWrite).to.equal('rw') + return expect(this.project.tokens.readOnly).to.equal('ro') + }) + }) + + describe('getDocPublishedInfo', function() { + beforeEach(function() { + return (this.callback = sinon.stub()) + }) + + describe('when v1 api not set', function() { + beforeEach(function() { + return this.TokenAccessHandler.getV1DocPublishedInfo( + this.token, + this.callback + ) + }) + + return it('should not check access and return default info', function() { + expect(this.V1Api.request.called).to.equal(false) + return expect( + this.callback.calledWith(null, { + allow: true + }) + ).to.equal(true) + }) + }) + + return describe('when v1 api is set', function() { + beforeEach(function() { + return (this.settings.apis = { v1: 'v1' }) + }) + + describe('on V1Api.request success', function() { + beforeEach(function() { + this.V1Api.request = sinon + .stub() + .callsArgWith(1, null, null, 'mock-data') + return this.TokenAccessHandler.getV1DocPublishedInfo( + this.token, + this.callback + ) + }) + + return it('should return response body', function() { + expect( + this.V1Api.request.calledWith({ + url: `/api/v1/sharelatex/docs/${this.token}/is_published` + }) + ).to.equal(true) + return expect(this.callback.calledWith(null, 'mock-data')).to.equal( + true + ) + }) + }) + + return describe('on V1Api.request error', function() { + beforeEach(function() { + this.V1Api.request = sinon.stub().callsArgWith(1, 'error') + return this.TokenAccessHandler.getV1DocPublishedInfo( + this.token, + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback.calledWith('error')).to.equal(true) + }) + }) + }) + }) + + return describe('getV1DocInfo', function() { + beforeEach(function() { + this.v2UserId = 123 + return (this.callback = sinon.stub()) + }) + + describe('when v1 api not set', function() { + beforeEach(function() { + return this.TokenAccessHandler.getV1DocInfo( + this.token, + this.v2UserId, + this.callback + ) + }) + + return it('should not check access and return default info', function() { + expect(this.V1Api.request.called).to.equal(false) + return expect( + this.callback.calledWith(null, { + exists: true, + exported: false + }) + ).to.equal(true) + }) + }) + + return describe('when v1 api is set', function() { + beforeEach(function() { + return (this.settings.apis = { v1: 'v1' }) + }) + + describe('on UserGetter.getUser success', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon.stub().yields(null, { + overleaf: { id: 1 } + }) + return this.TokenAccessHandler.getV1DocInfo( + this.token, + this.v2UserId, + this.callback + ) + }) + + return it('should get user', function() { + return expect( + this.UserGetter.getUser.calledWith(this.v2UserId) + ).to.equal(true) + }) + }) + + describe('on UserGetter.getUser error', function() { + beforeEach(function() { + this.error = new Error('failed to get user') + this.UserGetter.getUser = sinon.stub().yields(this.error) + return this.TokenAccessHandler.getV1DocInfo( + this.token, + this.v2UserId, + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback.calledWith(this.error)).to.equal(true) + }) + }) + + describe('on V1Api.request success', function() { + beforeEach(function() { + this.v1UserId = 1 + this.UserGetter.getUser = sinon.stub().yields(null, { + overleaf: { id: this.v1UserId } + }) + this.V1Api.request = sinon + .stub() + .callsArgWith(1, null, null, 'mock-data') + return this.TokenAccessHandler.getV1DocInfo( + this.token, + this.v2UserId, + this.callback + ) + }) + + return it('should return response body', function() { + expect( + this.V1Api.request.calledWith({ + url: `/api/v1/sharelatex/users/${this.v1UserId}/docs/${ + this.token + }/info` + }) + ).to.equal(true) + return expect(this.callback.calledWith(null, 'mock-data')).to.equal( + true + ) + }) + }) + + return describe('on V1Api.request error', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon.stub().yields(null, { + overleaf: { id: 1 } + }) + this.V1Api.request = sinon.stub().callsArgWith(1, 'error') + return this.TokenAccessHandler.getV1DocInfo( + this.token, + this.v2UserId, + this.callback + ) + }) + + return it('should callback with error', function() { + return expect(this.callback.calledWith('error')).to.equal(true) + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Uploads/ArchiveManagerTests.js b/services/web/test/unit/src/Uploads/ArchiveManagerTests.js new file mode 100644 index 0000000000..1d576e164c --- /dev/null +++ b/services/web/test/unit/src/Uploads/ArchiveManagerTests.js @@ -0,0 +1,538 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const { expect } = require('chai') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/src/Features/Uploads/ArchiveManager.js' +const Errors = require('../../../../app/src/Features/Errors/Errors') +const SandboxedModule = require('sandboxed-module') +const events = require('events') + +describe('ArchiveManager', function() { + beforeEach(function() { + let Timer + this.logger = { + error: sinon.stub(), + warn: sinon.stub(), + err() {}, + log: sinon.stub() + } + this.metrics = { + Timer: (Timer = (function() { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + } + this.zipfile = new events.EventEmitter() + this.zipfile.readEntry = sinon.stub() + this.zipfile.close = sinon.stub() + + this.ArchiveManager = SandboxedModule.require(modulePath, { + requires: { + yauzl: (this.yauzl = { + open: sinon.stub().callsArgWith(2, null, this.zipfile) + }), + 'logger-sharelatex': this.logger, + 'metrics-sharelatex': this.metrics, + fs: (this.fs = {}), + 'fs-extra': (this.fse = {}) + } + }) + return (this.callback = sinon.stub()) + }) + + describe('extractZipArchive', function() { + beforeEach(function() { + this.source = '/path/to/zip/source.zip' + this.destination = '/path/to/zip/destination' + return (this.ArchiveManager._isZipTooLarge = sinon + .stub() + .callsArgWith(1, null, false)) + }) + + describe('successfully', function() { + beforeEach(function(done) { + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + done + ) + return this.zipfile.emit('end') + }) + + it('should run yauzl', function() { + return this.yauzl.open.calledWith(this.source).should.equal(true) + }) + + it('should time the unzip', function() { + return this.metrics.Timer.prototype.done.called.should.equal(true) + }) + + return it('should log the unzip', function() { + return this.logger.log + .calledWith(sinon.match.any, 'unzipping file') + .should.equal(true) + }) + }) + + describe('with an error in the zip file header', function() { + beforeEach(function(done) { + this.yauzl.open = sinon + .stub() + .callsArgWith(2, new Errors.InvalidError('invalid_zip_file')) + return this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + }) + + it('should return the callback with an error', function() { + return sinon.assert.calledWithExactly( + this.callback, + new Errors.InvalidError('invalid_zip_file') + ) + }) + + return it('should log out the error', function() { + return this.logger.error.called.should.equal(true) + }) + }) + + describe('with a zip that is too large', function() { + beforeEach(function(done) { + this.ArchiveManager._isZipTooLarge = sinon + .stub() + .callsArgWith(1, null, true) + return this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + }) + + it('should return the callback with an error', function() { + return sinon.assert.calledWithExactly( + this.callback, + new Errors.InvalidError('zip_contents_too_large') + ) + }) + + return it('should not call yauzl.open', function() { + return this.yauzl.open.called.should.equal(false) + }) + }) + + describe('with an error in the extracted files', function() { + beforeEach(function(done) { + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + return this.zipfile.emit('error', new Error('Something went wrong')) + }) + + it('should return the callback with an error', function() { + return this.callback + .calledWithExactly(new Error('Something went wrong')) + .should.equal(true) + }) + + return it('should log out the error', function() { + return this.logger.error.called.should.equal(true) + }) + }) + + describe('with a relative extracted file path', function() { + beforeEach(function(done) { + this.zipfile.openReadStream = sinon.stub() + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + this.zipfile.emit('entry', { fileName: '../testfile.txt' }) + return this.zipfile.emit('end') + }) + + it('should not write try to read the file entry', function() { + return this.zipfile.openReadStream.called.should.equal(false) + }) + + return it('should log out a warning', function() { + return this.logger.warn.called.should.equal(true) + }) + }) + + describe('with an unnormalized extracted file path', function() { + beforeEach(function(done) { + this.zipfile.openReadStream = sinon.stub() + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + this.zipfile.emit('entry', { fileName: 'foo/./testfile.txt' }) + return this.zipfile.emit('end') + }) + + it('should not try to read the file entry', function() { + return this.zipfile.openReadStream.called.should.equal(false) + }) + + return it('should log out a warning', function() { + return this.logger.warn.called.should.equal(true) + }) + }) + + describe('with backslashes in the path', function() { + beforeEach(function(done) { + this.readStream = new events.EventEmitter() + this.readStream.pipe = sinon.stub() + this.writeStream = new events.EventEmitter() + this.fs.createWriteStream = sinon.stub().returns(this.writeStream) + this.zipfile.openReadStream = sinon + .stub() + .callsArgWith(1, null, this.readStream) + this.fse.ensureDir = sinon.stub().callsArg(1) + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + this.zipfile.emit('entry', { fileName: 'wombat\\foo.tex' }) + this.zipfile.emit('entry', { fileName: 'potato\\bar.tex' }) + return this.zipfile.emit('end') + }) + + it('should read the file entry with its original path', function() { + this.zipfile.openReadStream.should.be.calledWith({ + fileName: 'wombat\\foo.tex' + }) + return this.zipfile.openReadStream.should.be.calledWith({ + fileName: 'potato\\bar.tex' + }) + }) + + it('should treat the backslashes as a directory separator when creating the directory', function() { + this.fse.ensureDir.should.be.calledWith(`${this.destination}/wombat`) + return this.fse.ensureDir.should.be.calledWith( + `${this.destination}/potato` + ) + }) + + return it('should treat the backslashes as a directory separator when creating the file', function() { + this.fs.createWriteStream.should.be.calledWith( + `${this.destination}/wombat/foo.tex` + ) + return this.fs.createWriteStream.should.be.calledWith( + `${this.destination}/potato/bar.tex` + ) + }) + }) + + describe('with a directory entry', function() { + beforeEach(function(done) { + this.zipfile.openReadStream = sinon.stub() + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + this.zipfile.emit('entry', { fileName: 'testdir/' }) + return this.zipfile.emit('end') + }) + + it('should not try to read the entry', function() { + return this.zipfile.openReadStream.called.should.equal(false) + }) + + return it('should not log out a warning', function() { + return this.logger.warn.called.should.equal(false) + }) + }) + + describe('with an error opening the file read stream', function() { + beforeEach(function(done) { + this.zipfile.openReadStream = sinon + .stub() + .callsArgWith(1, new Error('Something went wrong')) + this.writeStream = new events.EventEmitter() + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + this.zipfile.emit('entry', { fileName: 'testfile.txt' }) + return this.zipfile.emit('end') + }) + + it('should return the callback with an error', function() { + return this.callback + .calledWithExactly(new Error('Something went wrong')) + .should.equal(true) + }) + + it('should log out the error', function() { + return this.logger.error.called.should.equal(true) + }) + + return it('should close the zipfile', function() { + return this.zipfile.close.called.should.equal(true) + }) + }) + + describe('with an error in the file read stream', function() { + beforeEach(function(done) { + this.readStream = new events.EventEmitter() + this.readStream.pipe = sinon.stub() + this.zipfile.openReadStream = sinon + .stub() + .callsArgWith(1, null, this.readStream) + this.writeStream = new events.EventEmitter() + this.fs.createWriteStream = sinon.stub().returns(this.writeStream) + this.fse.ensureDir = sinon.stub().callsArg(1) + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + this.zipfile.emit('entry', { fileName: 'testfile.txt' }) + this.readStream.emit('error', new Error('Something went wrong')) + return this.zipfile.emit('end') + }) + + it('should return the callback with an error', function() { + return this.callback + .calledWithExactly(new Error('Something went wrong')) + .should.equal(true) + }) + + it('should log out the error', function() { + return this.logger.error.called.should.equal(true) + }) + + return it('should close the zipfile', function() { + return this.zipfile.close.called.should.equal(true) + }) + }) + + return describe('with an error in the file write stream', function() { + beforeEach(function(done) { + this.readStream = new events.EventEmitter() + this.readStream.pipe = sinon.stub() + this.readStream.unpipe = sinon.stub() + this.readStream.destroy = sinon.stub() + this.zipfile.openReadStream = sinon + .stub() + .callsArgWith(1, null, this.readStream) + this.writeStream = new events.EventEmitter() + this.fs.createWriteStream = sinon.stub().returns(this.writeStream) + this.fse.ensureDir = sinon.stub().callsArg(1) + this.ArchiveManager.extractZipArchive( + this.source, + this.destination, + error => { + this.callback(error) + return done() + } + ) + this.zipfile.emit('entry', { fileName: 'testfile.txt' }) + this.writeStream.emit('error', new Error('Something went wrong')) + return this.zipfile.emit('end') + }) + + it('should return the callback with an error', function() { + return this.callback + .calledWithExactly(new Error('Something went wrong')) + .should.equal(true) + }) + + it('should log out the error', function() { + return this.logger.error.called.should.equal(true) + }) + + it('should unpipe from the readstream', function() { + return this.readStream.unpipe.called.should.equal(true) + }) + + it('should destroy the readstream', function() { + return this.readStream.destroy.called.should.equal(true) + }) + + return it('should close the zipfile', function() { + return this.zipfile.close.called.should.equal(true) + }) + }) + }) + + describe('_isZipTooLarge', function() { + it('should return false with small output', function(done) { + this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => { + isTooLarge.should.equal(false) + return done() + }) + this.zipfile.emit('entry', { uncompressedSize: 109042 }) + return this.zipfile.emit('end') + }) + + it('should return true with large bytes', function(done) { + this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => { + isTooLarge.should.equal(true) + return done() + }) + this.zipfile.emit('entry', { uncompressedSize: 1090000000000000042 }) + return this.zipfile.emit('end') + }) + + it('should return error on no data', function(done) { + this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => { + expect(error).to.exist + return done() + }) + this.zipfile.emit('entry', {}) + return this.zipfile.emit('end') + }) + + it("should return error if it didn't get a number", function(done) { + this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => { + expect(error).to.exist + return done() + }) + this.zipfile.emit('entry', { uncompressedSize: 'random-error' }) + return this.zipfile.emit('end') + }) + + return it('should return error if there is no data', function(done) { + this.ArchiveManager._isZipTooLarge(this.source, (error, isTooLarge) => { + expect(error).to.exist + return done() + }) + return this.zipfile.emit('end') + }) + }) + + return describe('findTopLevelDirectory', function() { + beforeEach(function() { + this.fs.readdir = sinon.stub() + this.fs.stat = sinon.stub() + return (this.directory = 'test/directory') + }) + + describe('with multiple files', function() { + beforeEach(function() { + this.fs.readdir.callsArgWith(1, null, ['multiple', 'files']) + return this.ArchiveManager.findTopLevelDirectory( + this.directory, + this.callback + ) + }) + + it('should find the files in the directory', function() { + return this.fs.readdir.calledWith(this.directory).should.equal(true) + }) + + return it('should return the original directory', function() { + return this.callback.calledWith(null, this.directory).should.equal(true) + }) + }) + + describe('with a single file (not folder)', function() { + beforeEach(function() { + this.fs.readdir.callsArgWith(1, null, ['foo.tex']) + this.fs.stat.callsArgWith(1, null, { + isDirectory() { + return false + } + }) + return this.ArchiveManager.findTopLevelDirectory( + this.directory, + this.callback + ) + }) + + it('should check if the file is a directory', function() { + return this.fs.stat + .calledWith(this.directory + '/foo.tex') + .should.equal(true) + }) + + return it('should return the original directory', function() { + return this.callback.calledWith(null, this.directory).should.equal(true) + }) + }) + + return describe('with a single top-level folder', function() { + beforeEach(function() { + this.fs.readdir.callsArgWith(1, null, ['folder']) + this.fs.stat.callsArgWith(1, null, { + isDirectory() { + return true + } + }) + return this.ArchiveManager.findTopLevelDirectory( + this.directory, + this.callback + ) + }) + + it('should check if the file is a directory', function() { + return this.fs.stat + .calledWith(this.directory + '/folder') + .should.equal(true) + }) + + return it('should return the child directory', function() { + return this.callback + .calledWith(null, this.directory + '/folder') + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Uploads/FileSystemImportManagerTests.js b/services/web/test/unit/src/Uploads/FileSystemImportManagerTests.js new file mode 100644 index 0000000000..50c6eb6120 --- /dev/null +++ b/services/web/test/unit/src/Uploads/FileSystemImportManagerTests.js @@ -0,0 +1,549 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = + '../../../../app/src/Features/Uploads/FileSystemImportManager.js' +const SandboxedModule = require('sandboxed-module') + +describe('FileSystemImportManager', function() { + beforeEach(function() { + this.project_id = 'project-id-123' + this.folder_id = 'folder-id-123' + this.name = 'test-file.tex' + this.path_on_disk = `/path/to/file/${this.name}` + this.replace = 'replace-boolean-flag-mock' + this.user_id = 'mock-user-123' + this.callback = sinon.stub() + this.encoding = 'latin1' + this.DocumentHelper = { + convertTexEncodingsToUtf8: sinon.stub().returnsArg(0) + } + return (this.FileSystemImportManager = SandboxedModule.require(modulePath, { + requires: { + fs: (this.fs = {}), + '../Editor/EditorController': (this.EditorController = {}), + './FileTypeManager': (this.FileTypeManager = {}), + '../Project/ProjectLocator': (this.ProjectLocator = {}), + '../Documents/DocumentHelper': this.DocumentHelper, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + describe('addDoc', function() { + beforeEach(function() { + this.docContent = 'one\ntwo\nthree' + this.docLines = this.docContent.split('\n') + this.fs.readFile = sinon.stub().callsArgWith(2, null, this.docContent) + return (this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true)) + }) + + describe('when path is symlink', function() { + beforeEach(function() { + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, false) + this.EditorController.addDoc = sinon.stub() + return this.FileSystemImportManager.addDoc( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.encoding, + false, + this.callback + ) + }) + + it('should not read the file from disk', function() { + return this.fs.readFile.called.should.equal(false) + }) + + return it('should not insert the doc', function() { + return this.EditorController.addDoc.called.should.equal(false) + }) + }) + + describe('with replace set to false', function() { + beforeEach(function() { + this.EditorController.addDoc = sinon.stub().callsArg(6) + return this.FileSystemImportManager.addDoc( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.encoding, + false, + this.callback + ) + }) + + it('should read the file from disk', function() { + return this.fs.readFile.calledWith(this.path_on_disk).should.equal(true) + }) + + return it('should insert the doc', function() { + return this.EditorController.addDoc + .calledWith( + this.project_id, + this.folder_id, + this.name, + this.docLines, + 'upload', + this.user_id + ) + .should.equal(true) + }) + }) + + describe('with windows line ending', function() { + beforeEach(function() { + this.docContent = 'one\r\ntwo\r\nthree' + this.docLines = ['one', 'two', 'three'] + this.fs.readFile = sinon.stub().callsArgWith(2, null, this.docContent) + this.EditorController.addDoc = sinon.stub().callsArg(6) + return this.FileSystemImportManager.addDoc( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.encoding, + false, + this.callback + ) + }) + + return it('should strip the \\r characters before adding', function() { + return this.EditorController.addDoc + .calledWith( + this.project_id, + this.folder_id, + this.name, + this.docLines, + 'upload', + this.user_id + ) + .should.equal(true) + }) + }) + + describe('with \r line endings', function() { + beforeEach(function() { + this.docContent = 'one\rtwo\rthree' + this.docLines = ['one', 'two', 'three'] + this.fs.readFile = sinon.stub().callsArgWith(2, null, this.docContent) + this.EditorController.addDoc = sinon.stub().callsArg(6) + return this.FileSystemImportManager.addDoc( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.encoding, + false, + this.callback + ) + }) + + return it('should treat the \\r characters as newlines', function() { + return this.EditorController.addDoc + .calledWith( + this.project_id, + this.folder_id, + this.name, + this.docLines, + 'upload', + this.user_id + ) + .should.equal(true) + }) + }) + + return describe('with replace set to true', function() { + beforeEach(function() { + this.EditorController.upsertDoc = sinon.stub().yields() + return this.FileSystemImportManager.addDoc( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.encoding, + true, + this.callback + ) + }) + + it('should upsert the doc', function() { + return this.EditorController.upsertDoc + .calledWith( + this.project_id, + this.folder_id, + this.name, + this.docLines, + 'upload', + this.user_id + ) + .should.equal(true) + }) + + return it('should read the file with the correct encoding', function() { + return sinon.assert.calledWith( + this.fs.readFile, + this.path_on_disk, + this.encoding + ) + }) + }) + }) + + describe('addFile with replace set to false', function() { + beforeEach(function() { + this.EditorController.addFile = sinon.stub().yields() + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true) + return this.FileSystemImportManager.addFile( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + false, + this.callback + ) + }) + + return it('should add the file', function() { + return this.EditorController.addFile + .calledWith( + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + null, + 'upload', + this.user_id + ) + .should.equal(true) + }) + }) + + describe('addFile with symlink', function() { + beforeEach(function() { + this.EditorController.addFile = sinon.stub() + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, false) + this.EditorController.replaceFile = sinon.stub() + return this.FileSystemImportManager.addFile( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + false, + this.callback + ) + }) + + return it('should node add the file', function() { + this.EditorController.addFile.called.should.equal(false) + return this.EditorController.replaceFile.called.should.equal(false) + }) + }) + + describe('addFile with replace set to true', function() { + beforeEach(function() { + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true) + this.EditorController.upsertFile = sinon.stub().yields() + return this.FileSystemImportManager.addFile( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + true, + this.callback + ) + }) + + return it('should add the file', function() { + return this.EditorController.upsertFile + .calledWith( + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + null, + 'upload', + this.user_id + ) + .should.equal(true) + }) + }) + + describe('addFolder', function() { + beforeEach(function() { + this.new_folder_id = 'new-folder-id' + this.EditorController.addFolder = sinon + .stub() + .callsArgWith(4, null, { _id: this.new_folder_id }) + return (this.FileSystemImportManager.addFolderContents = sinon + .stub() + .callsArg(5)) + }) + + describe('successfully', function() { + beforeEach(function() { + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true) + return this.FileSystemImportManager.addFolder( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.replace, + this.callback + ) + }) + + it('should add a folder to the project', function() { + return this.EditorController.addFolder + .calledWith(this.project_id, this.folder_id, this.name, 'upload') + .should.equal(true) + }) + + return it('should add the folders contents', function() { + return this.FileSystemImportManager.addFolderContents + .calledWith( + this.user_id, + this.project_id, + this.new_folder_id, + this.path_on_disk, + this.replace + ) + .should.equal(true) + }) + }) + + return describe('with symlink', function() { + beforeEach(function() { + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, false) + return this.FileSystemImportManager.addFolder( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.replace, + this.callback + ) + }) + + return it('should not add a folder to the project', function() { + this.EditorController.addFolder.called.should.equal(false) + return this.FileSystemImportManager.addFolderContents.called.should.equal( + false + ) + }) + }) + }) + + describe('addFolderContents', function() { + beforeEach(function() { + this.folderEntries = ['path1', 'path2', 'path3'] + this.ignoredEntries = ['.DS_Store'] + this.fs.readdir = sinon + .stub() + .callsArgWith(1, null, this.folderEntries.concat(this.ignoredEntries)) + this.FileSystemImportManager.addEntity = sinon.stub().callsArg(6) + this.FileTypeManager.shouldIgnore = (path, callback) => { + return callback( + null, + this.ignoredEntries.indexOf(require('path').basename(path)) !== -1 + ) + } + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true) + return this.FileSystemImportManager.addFolderContents( + this.user_id, + this.project_id, + this.folder_id, + this.path_on_disk, + this.replace, + this.callback + ) + }) + + it('should call addEntity for each file in the folder which is not ignored', function() { + return Array.from(this.folderEntries).map(name => + this.FileSystemImportManager.addEntity + .calledWith( + this.user_id, + this.project_id, + this.folder_id, + name, + `${this.path_on_disk}/${name}`, + this.replace + ) + .should.equal(true) + ) + }) + + it('should not call addEntity for the ignored files', function() { + return Array.from(this.ignoredEntries).map(name => + this.FileSystemImportManager.addEntity + .calledWith( + this.user_id, + this.project_id, + this.folder_id, + name, + `${this.path_on_disk}/${name}`, + this.replace + ) + .should.equal(false) + ) + }) + + return it('should look in the correct directory', function() { + return this.fs.readdir.calledWith(this.path_on_disk).should.equal(true) + }) + }) + + return describe('addEntity', function() { + describe('with directory', function() { + beforeEach(function() { + this.FileTypeManager.isDirectory = sinon + .stub() + .callsArgWith(1, null, true) + this.FileSystemImportManager.addFolder = sinon.stub().callsArg(6) + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true) + return this.FileSystemImportManager.addEntity( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.replace, + this.callback + ) + }) + + return it('should call addFolder', function() { + return this.FileSystemImportManager.addFolder + .calledWith( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.replace + ) + .should.equal(true) + }) + }) + + describe('with binary file', function() { + beforeEach(function() { + this.FileTypeManager.isDirectory = sinon + .stub() + .callsArgWith(1, null, false) + this.FileTypeManager.getType = sinon.stub().callsArgWith(2, null, true) + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true) + this.FileSystemImportManager.addFile = sinon.stub().callsArg(6) + return this.FileSystemImportManager.addEntity( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.replace, + this.callback + ) + }) + + return it('should call addFile', function() { + return this.FileSystemImportManager.addFile + .calledWith( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.replace + ) + .should.equal(true) + }) + }) + + return describe('with text file', function() { + beforeEach(function() { + this.FileTypeManager.isDirectory = sinon + .stub() + .callsArgWith(1, null, false) + this.FileTypeManager.getType = sinon + .stub() + .callsArgWith(2, null, false, 'latin1') + this.FileSystemImportManager.addDoc = sinon.stub().callsArg(7) + this.FileSystemImportManager._isSafeOnFileSystem = sinon + .stub() + .callsArgWith(1, null, true) + return this.FileSystemImportManager.addEntity( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + this.replace, + this.callback + ) + }) + + return it('should call addFile', function() { + return sinon.assert.calledWith( + this.FileSystemImportManager.addDoc, + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path_on_disk, + 'latin1', + this.replace + ) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Uploads/FileTypeManagerTests.js b/services/web/test/unit/src/Uploads/FileTypeManagerTests.js new file mode 100644 index 0000000000..459a231967 --- /dev/null +++ b/services/web/test/unit/src/Uploads/FileTypeManagerTests.js @@ -0,0 +1,325 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/src/Features/Uploads/FileTypeManager.js' +const SandboxedModule = require('sandboxed-module') +const isUtf8 = require('is-utf8') + +describe('FileTypeManager', function() { + beforeEach(function() { + this.isUtf8 = sinon.spy(isUtf8) + this.fs = {} + this.path = '/path/to/test' + this.callback = sinon.stub() + this.ced = sinon.stub() + this.DocumentHelper = { getEncodingFromTexContent: sinon.stub() } + return (this.FileTypeManager = SandboxedModule.require(modulePath, { + requires: { + fs: this.fs, + 'is-utf8': this.isUtf8 + } + })) + }) + + describe('isDirectory', function() { + beforeEach(function() { + this.stats = {} + return (this.fs.stat = sinon.stub().callsArgWith(1, null, this.stats)) + }) + + describe('when it is a directory', function() { + beforeEach(function() { + this.stats.isDirectory = sinon.stub().returns(true) + return this.FileTypeManager.isDirectory(this.path, this.callback) + }) + + return it('should return true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + return describe('when it is not a directory', function() { + beforeEach(function() { + this.stats.isDirectory = sinon.stub().returns(false) + return this.FileTypeManager.isDirectory(this.path, this.callback) + }) + + return it('should return false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) + }) + + describe('getType', function() { + beforeEach(function() { + this.stat = { size: 100 } + this.contents = 'Ich bin eine kleine Teekanne, kurz und kräftig.' + this.fs.stat = sinon.stub().callsArgWith(1, null, this.stat) + this.fs.readFile = sinon + .stub() + .callsArgWith(1, null, Buffer.from(this.contents, 'utf-8')) + this.fs.readFile + .withArgs('/path/on/disk/utf16.tex') + .callsArgWith( + 1, + null, + Buffer.from(`\uFEFF${this.contents}`, 'utf-16le') + ) + this.fs.readFile + .withArgs('/path/on/disk/latin1.tex') + .callsArgWith(1, null, Buffer.from(this.contents, 'latin1')) + return (this.encoding = 'ASCII') + }) + + describe('when the file extension is text', function() { + it('should return .tex files as not binary', function() { + return this.FileTypeManager.getType( + 'file.tex', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return .bib files as not binary', function() { + return this.FileTypeManager.getType( + 'file.bib', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return .bibtex files as not binary', function() { + return this.FileTypeManager.getType( + 'file.bibtex', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return .cls files as not binary', function() { + return this.FileTypeManager.getType( + 'file.cls', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return .sty files as not binary', function() { + return this.FileTypeManager.getType( + 'file.sty', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return .bst files as not binary', function() { + return this.FileTypeManager.getType( + 'file.bst', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return .latexmkrc file as not binary', function() { + return this.FileTypeManager.getType( + '.latexmkrc', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return latexmkrc file as not binary', function() { + return this.FileTypeManager.getType( + 'latexmkrc', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return lbx file as not binary', function() { + return this.FileTypeManager.getType( + 'file.lbx', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return bbx file as not binary', function() { + return this.FileTypeManager.getType( + 'file.bbx', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return cbx file as not binary', function() { + return this.FileTypeManager.getType( + 'file.cbx', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return m file as not binary', function() { + return this.FileTypeManager.getType( + 'file.m', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should ignore the case of an extension', function() { + return this.FileTypeManager.getType( + 'file.TEX', + '/path/on/disk', + (error, binary) => binary.should.equal(false) + ) + }) + + it('should return large text files as binary', function() { + this.stat.size = 2 * 1024 * 1024 // 2Mb + return this.FileTypeManager.getType( + 'file.tex', + '/path/on/disk', + (error, binary) => binary.should.equal(true) + ) + }) + + it('should return try to determine the encoding of large files', function() { + this.stat.size = 2 * 1024 * 1024 // 2Mb + return this.FileTypeManager.getType('file.tex', '/path/on/disk', () => { + return sinon.assert.notCalled(this.isUtf8) + }) + }) + + it('should detect the file as utf8', function() { + return this.FileTypeManager.getType( + 'file.tex', + '/path/on/disk', + (error, binary, encoding) => { + sinon.assert.calledOnce(this.isUtf8) + this.isUtf8.returned(true).should.equal(true) + return encoding.should.equal('utf-8') + } + ) + }) + + it("should return 'latin1' for non-unicode encodings", function() { + return this.FileTypeManager.getType( + 'file.tex', + '/path/on/disk/latin1.tex', + (error, binary, encoding) => { + sinon.assert.calledOnce(this.isUtf8) + this.isUtf8.returned(false).should.equal(true) + return encoding.should.equal('latin1') + } + ) + }) + + return it('should detect utf16 with BOM as utf-16', function() { + return this.FileTypeManager.getType( + 'file.tex', + '/path/on/disk/utf16.tex', + (error, binary, encoding) => { + sinon.assert.calledOnce(this.isUtf8) + this.isUtf8.returned(false).should.equal(true) + return encoding.should.equal('utf-16le') + } + ) + }) + }) + + return describe('when the file extension is non-text', function() { + it('should return .eps files as binary', function() { + return this.FileTypeManager.getType( + 'file.eps', + '/path/on/disk', + (error, binary) => binary.should.equal(true) + ) + }) + + it('should return .dvi files as binary', function() { + return this.FileTypeManager.getType( + 'file.dvi', + '/path/on/disk', + (error, binary) => binary.should.equal(true) + ) + }) + + it('should return .png files as binary', function() { + return this.FileTypeManager.getType( + 'file.png', + '/path/on/disk', + (error, binary) => binary.should.equal(true) + ) + }) + + it('should return files without extensions as binary', function() { + return this.FileTypeManager.getType( + 'tex', + '/path/on/disk', + (error, binary) => binary.should.equal(true) + ) + }) + + return it('should not try to get the character encoding', function() { + return this.FileTypeManager.getType('file.png', '/path/on/disk', () => { + return sinon.assert.notCalled(this.isUtf8) + }) + }) + }) + }) + + return describe('shouldIgnore', function() { + it('should ignore tex auxiliary files', function() { + return this.FileTypeManager.shouldIgnore('file.aux', (error, ignore) => + ignore.should.equal(true) + ) + }) + + it('should ignore dotfiles', function() { + return this.FileTypeManager.shouldIgnore('path/.git', (error, ignore) => + ignore.should.equal(true) + ) + }) + + it('should not ignore .latexmkrc dotfile', function() { + return this.FileTypeManager.shouldIgnore( + 'path/.latexmkrc', + (error, ignore) => ignore.should.equal(false) + ) + }) + + it('should ignore __MACOSX', function() { + return this.FileTypeManager.shouldIgnore( + 'path/__MACOSX', + (error, ignore) => ignore.should.equal(true) + ) + }) + + it('should not ignore .tex files', function() { + return this.FileTypeManager.shouldIgnore('file.tex', (error, ignore) => + ignore.should.equal(false) + ) + }) + + return it('should ignore the case of the extension', function() { + return this.FileTypeManager.shouldIgnore('file.AUX', (error, ignore) => + ignore.should.equal(true) + ) + }) + }) +}) diff --git a/services/web/test/unit/src/Uploads/ProjectUploadControllerTests.js b/services/web/test/unit/src/Uploads/ProjectUploadControllerTests.js new file mode 100644 index 0000000000..3058dd5dfb --- /dev/null +++ b/services/web/test/unit/src/Uploads/ProjectUploadControllerTests.js @@ -0,0 +1,281 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = + '../../../../app/src/Features/Uploads/ProjectUploadController.js' +const SandboxedModule = require('sandboxed-module') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('ProjectUploadController', function() { + beforeEach(function() { + let Timer + this.req = new MockRequest() + this.res = new MockResponse() + this.user_id = 'user-id-123' + this.metrics = { + Timer: (Timer = (function() { + Timer = class Timer { + static initClass() { + this.prototype.done = sinon.stub() + } + } + Timer.initClass() + return Timer + })()) + } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user_id) + } + + return (this.ProjectUploadController = SandboxedModule.require(modulePath, { + requires: { + './ProjectUploadManager': (this.ProjectUploadManager = {}), + './FileSystemImportManager': (this.FileSystemImportManager = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + err() {} + }), + 'metrics-sharelatex': this.metrics, + '../Authentication/AuthenticationController': this + .AuthenticationController, + fs: (this.fs = {}) + } + })) + }) + + describe('uploadProject', function() { + beforeEach(function() { + this.path = '/path/to/file/on/disk.zip' + this.name = 'filename.zip' + this.req.file = { + path: this.path, + originalname: this.name + } + this.req.session = { + user: { + _id: this.user_id + } + } + this.project = { _id: (this.project_id = 'project-id-123') } + + return (this.fs.unlink = sinon.stub()) + }) + + describe('successfully', function() { + beforeEach(function() { + this.ProjectUploadManager.createProjectFromZipArchive = sinon + .stub() + .callsArgWith(3, null, this.project) + return this.ProjectUploadController.uploadProject(this.req, this.res) + }) + + it('should create a project owned by the logged in user', function() { + return this.ProjectUploadManager.createProjectFromZipArchive + .calledWith(this.user_id) + .should.equal(true) + }) + + it('should create a project with the same name as the zip archive', function() { + return this.ProjectUploadManager.createProjectFromZipArchive + .calledWith(sinon.match.any, 'filename', sinon.match.any) + .should.equal(true) + }) + + it('should create a project from the zip archive', function() { + return this.ProjectUploadManager.createProjectFromZipArchive + .calledWith(sinon.match.any, sinon.match.any, this.path) + .should.equal(true) + }) + + it('should return a successful response to the FileUploader client', function() { + return expect(this.res.body).to.deep.equal({ + success: true, + project_id: this.project_id + }) + }) + + it('should record the time taken to do the upload', function() { + return this.metrics.Timer.prototype.done.called.should.equal(true) + }) + + it('should output a log line', function() { + return this.logger.log + .calledWith(sinon.match.any, 'uploaded project') + .should.equal(true) + }) + + return it('should remove the uploaded file', function() { + return this.fs.unlink.calledWith(this.path).should.equal(true) + }) + }) + + describe('when ProjectUploadManager.createProjectFromZipArchive fails', function() { + beforeEach(function() { + this.ProjectUploadManager.createProjectFromZipArchive = sinon + .stub() + .callsArgWith(3, new Error('Something went wrong'), this.project) + return this.ProjectUploadController.uploadProject(this.req, this.res) + }) + + it('should return a failed response to the FileUploader client', function() { + return expect(this.res.body).to.deep.equal( + JSON.stringify({ success: false, error: 'upload_failed' }) + ) + }) + + return it('should output an error log line', function() { + return this.logger.error + .calledWith(sinon.match.any, 'error uploading project') + .should.equal(true) + }) + }) + + return describe('when ProjectUploadManager.createProjectFromZipArchive reports the file as invalid', function() { + beforeEach(function() { + this.ProjectUploadManager.createProjectFromZipArchive = sinon + .stub() + .callsArgWith( + 3, + new Errors.InvalidError('zip_contents_too_large'), + this.project + ) + return this.ProjectUploadController.uploadProject(this.req, this.res) + }) + + it('should return the reported error to the FileUploader client', function() { + return expect(this.res.body).to.deep.equal( + JSON.stringify({ success: false, error: 'zip_contents_too_large' }) + ) + }) + + it("should return an 'unprocessable entity' status code", function() { + return expect(this.res.statusCode).to.equal(422) + }) + + return it('should output an error log line', function() { + return this.logger.error + .calledWith(sinon.match.any, 'error uploading project') + .should.equal(true) + }) + }) + }) + + return describe('uploadFile', function() { + beforeEach(function() { + this.project_id = 'project-id-123' + this.folder_id = 'folder-id-123' + this.path = '/path/to/file/on/disk.png' + this.name = 'filename.png' + this.req.file = { + path: this.path, + originalname: this.name + } + this.req.session = { + user: { + _id: this.user_id + } + } + this.req.params = { Project_id: this.project_id } + this.req.query = { folder_id: this.folder_id } + return (this.fs.unlink = sinon.stub()) + }) + + describe('successfully', function() { + beforeEach(function() { + this.entity = { + _id: '1234', + type: 'file' + } + this.FileSystemImportManager.addEntity = sinon + .stub() + .callsArgWith(6, null, this.entity) + return this.ProjectUploadController.uploadFile(this.req, this.res) + }) + + it('should insert the file', function() { + return this.FileSystemImportManager.addEntity + .calledWith( + this.user_id, + this.project_id, + this.folder_id, + this.name, + this.path + ) + .should.equal(true) + }) + + it('should return a successful response to the FileUploader client', function() { + return expect(this.res.body).to.deep.equal({ + success: true, + entity_id: this.entity._id, + entity_type: 'file' + }) + }) + + it('should output a log line', function() { + return this.logger.log + .calledWith(sinon.match.any, 'uploaded file') + .should.equal(true) + }) + + it('should time the request', function() { + return this.metrics.Timer.prototype.done.called.should.equal(true) + }) + + return it('should remove the uploaded file', function() { + return this.fs.unlink.calledWith(this.path).should.equal(true) + }) + }) + + describe('when FileSystemImportManager.addEntity returns an error', function() { + beforeEach(function() { + this.FileSystemImportManager.addEntity = sinon + .stub() + .callsArgWith(6, new Error('Sorry something went wrong')) + return this.ProjectUploadController.uploadFile(this.req, this.res) + }) + + it('should return an unsuccessful response to the FileUploader client', function() { + return expect(this.res.body).to.deep.equal({ + success: false + }) + }) + + return it('should output an error log line', function() { + return this.logger.error + .calledWith(sinon.match.any, 'error uploading file') + .should.equal(true) + }) + }) + + return describe('with a bad request', function() { + beforeEach(function() { + this.req.file.originalname = '' + return this.ProjectUploadController.uploadFile(this.req, this.res) + }) + + return it('should return a a non success response', function() { + return expect(this.res.body).to.deep.equal({ + success: false + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/Uploads/ProjectUploadManagerTests.js b/services/web/test/unit/src/Uploads/ProjectUploadManagerTests.js new file mode 100644 index 0000000000..3589f5c243 --- /dev/null +++ b/services/web/test/unit/src/Uploads/ProjectUploadManagerTests.js @@ -0,0 +1,312 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = + '../../../../app/src/Features/Uploads/ProjectUploadManager.js' +const SandboxedModule = require('sandboxed-module') + +describe('ProjectUploadManager', function() { + beforeEach(function() { + this.project_id = 'project-id-123' + this.folder_id = 'folder-id-123' + this.owner_id = 'onwer-id-123' + this.callback = sinon.stub() + this.source = '/path/to/zip/file-name.zip' + this.destination = '/path/to/zile/file-extracted' + this.root_folder_id = this.folder_id + this.owner_id = 'owner-id-123' + this.name = 'Project name' + this.othername = 'Other name' + this.project = { + _id: this.project_id, + rootFolder: [{ _id: this.root_folder_id }] + } + this.ProjectUploadManager = SandboxedModule.require(modulePath, { + requires: { + './FileSystemImportManager': (this.FileSystemImportManager = {}), + './ArchiveManager': (this.ArchiveManager = {}), + '../Project/ProjectCreationHandler': (this.ProjectCreationHandler = {}), + '../Project/ProjectRootDocManager': (this.ProjectRootDocManager = {}), + '../Project/ProjectDetailsHandler': (this.ProjectDetailsHandler = {}), + '../Documents/DocumentHelper': (this.DocumentHelper = {}), + rimraf: (this.rimraf = sinon.stub().callsArg(1)) + } + }) + + this.ArchiveManager.extractZipArchive = sinon.stub().callsArg(2) + this.ArchiveManager.findTopLevelDirectory = sinon + .stub() + .callsArgWith( + 1, + null, + (this.topLevelDestination = '/path/to/zip/file-extracted/nested') + ) + this.ProjectCreationHandler.createBlankProject = sinon + .stub() + .callsArgWith(2, null, this.project) + this.ProjectRootDocManager.setRootDocAutomatically = sinon + .stub() + .callsArg(1) + this.FileSystemImportManager.addFolderContents = sinon.stub().callsArg(5) + this.ProjectRootDocManager.findRootDocFileFromDirectory = sinon + .stub() + .callsArgWith(1, null, 'main.tex', this.othername) + this.ProjectRootDocManager.setRootDocFromName = sinon.stub().callsArg(2) + this.DocumentHelper.getTitleFromTexContent = sinon + .stub() + .returns(this.othername) + return (this.ProjectDetailsHandler.fixProjectName = sinon + .stub() + .returnsArg(0)) + }) + + describe('createProjectFromZipArchive', function() { + describe('when the title can be read from the root document', function() { + beforeEach(function(done) { + this.ProjectUploadManager._getDestinationDirectory = sinon + .stub() + .returns(this.destination) + this.ProjectDetailsHandler.generateUniqueName = sinon + .stub() + .callsArgWith(2, null, this.othername) + return this.ProjectUploadManager.createProjectFromZipArchive( + this.owner_id, + this.name, + this.source, + (err, project) => { + this.callback(err, project) + return done() + } + ) + }) + + it('should set up the directory to extract the archive to', function() { + return this.ProjectUploadManager._getDestinationDirectory + .calledWith(this.source) + .should.equal(true) + }) + + it('should extract the archive', function() { + return this.ArchiveManager.extractZipArchive + .calledWith(this.source, this.destination) + .should.equal(true) + }) + + it('should find the top level directory', function() { + return this.ArchiveManager.findTopLevelDirectory + .calledWith(this.destination) + .should.equal(true) + }) + + it('should insert the extracted archive into the folder', function() { + return this.FileSystemImportManager.addFolderContents + .calledWith( + this.owner_id, + this.project_id, + this.folder_id, + this.topLevelDestination, + false + ) + .should.equal(true) + }) + + it('should create a project owned by the owner_id', function() { + return this.ProjectCreationHandler.createBlankProject + .calledWith(this.owner_id) + .should.equal(true) + }) + + it('should create a project with the correct name', function() { + return this.ProjectCreationHandler.createBlankProject + .calledWith(sinon.match.any, this.othername) + .should.equal(true) + }) + + it('should read the title from the tex contents', function() { + return this.DocumentHelper.getTitleFromTexContent.called.should.equal( + true + ) + }) + + it('should set the root document', function() { + return this.ProjectRootDocManager.setRootDocFromName + .calledWith(this.project_id, 'main.tex') + .should.equal(true) + }) + + it('should call the callback', function() { + return this.callback + .calledWith(sinon.match.falsy, this.project) + .should.equal(true) + }) + + return it('should ensure the name is valid', function() { + return this.ProjectDetailsHandler.fixProjectName.called.should.equal( + true + ) + }) + }) + + return describe("when the root document can't be determined", function() { + beforeEach(function(done) { + this.ProjectRootDocManager.findRootDocFileFromDirectory = sinon + .stub() + .callsArg(1) + this.ProjectUploadManager._getDestinationDirectory = sinon + .stub() + .returns(this.destination) + this.ProjectDetailsHandler.generateUniqueName = sinon + .stub() + .callsArgWith(2, null, this.name) + return this.ProjectUploadManager.createProjectFromZipArchive( + this.owner_id, + this.name, + this.source, + (err, project) => { + this.callback(err, project) + return done() + } + ) + }) + + return it('should not try to set the root doc', function() { + return this.ProjectRootDocManager.setRootDocFromName.called.should.equal( + false + ) + }) + }) + }) + + describe('createProjectFromZipArchiveWithName', function() { + beforeEach(function(done) { + this.ProjectDetailsHandler.generateUniqueName = sinon + .stub() + .callsArgWith(2, null, this.name) + this.ProjectUploadManager.insertZipArchiveIntoFolder = sinon + .stub() + .callsArg(4) + return this.ProjectUploadManager.createProjectFromZipArchiveWithName( + this.owner_id, + this.name, + this.source, + (err, project) => { + this.callback(err, project) + return done() + } + ) + }) + + it('should create a project owned by the owner_id', function() { + return this.ProjectCreationHandler.createBlankProject + .calledWith(this.owner_id) + .should.equal(true) + }) + + it('should create a project with the correct name', function() { + return this.ProjectCreationHandler.createBlankProject + .calledWith(sinon.match.any, this.name) + .should.equal(true) + }) + + it('should insert the zip file contents into the root folder', function() { + return this.ProjectUploadManager.insertZipArchiveIntoFolder + .calledWith( + this.owner_id, + this.project_id, + this.root_folder_id, + this.source + ) + .should.equal(true) + }) + + it('should automatically set the root doc', function() { + return this.ProjectRootDocManager.setRootDocAutomatically + .calledWith(this.project_id) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback + .calledWith(sinon.match.falsy, this.project) + .should.equal(true) + }) + }) + + describe('insertZipArchiveIntoFolder', function() { + beforeEach(function(done) { + this.ProjectUploadManager._getDestinationDirectory = sinon + .stub() + .returns(this.destination) + return this.ProjectUploadManager.insertZipArchiveIntoFolder( + this.owner_id, + this.project_id, + this.folder_id, + this.source, + err => { + this.callback(err) + return done() + } + ) + }) + + it('should set up the directory to extract the archive to', function() { + return this.ProjectUploadManager._getDestinationDirectory + .calledWith(this.source) + .should.equal(true) + }) + + it('should extract the archive', function() { + return this.ArchiveManager.extractZipArchive + .calledWith(this.source, this.destination) + .should.equal(true) + }) + + it('should find the top level directory', function() { + return this.ArchiveManager.findTopLevelDirectory + .calledWith(this.destination) + .should.equal(true) + }) + + it('should insert the extracted archive into the folder', function() { + return this.FileSystemImportManager.addFolderContents + .calledWith( + this.owner_id, + this.project_id, + this.folder_id, + this.topLevelDestination, + false + ) + .should.equal(true) + }) + + it('should return the callback', function() { + return this.callback.called.should.equal(true) + }) + + return it('should remove the desintation directory afterwards', function() { + return this.rimraf.calledWith(this.destination).should.equal(true) + }) + }) + + return describe('_getDestinationDirectory', () => + it('should return the path with the time appended', function() { + const date = Date.now() + sinon.stub(Date, 'now', () => date) + this.ProjectUploadManager._getDestinationDirectory( + '/path/to/zip/file.zip' + ).should.equal(`/path/to/zip/file-${date}`) + return Date.now.restore() + })) +}) diff --git a/services/web/test/unit/src/User/UserControllerTests.js b/services/web/test/unit/src/User/UserControllerTests.js new file mode 100644 index 0000000000..c8707bceaf --- /dev/null +++ b/services/web/test/unit/src/User/UserControllerTests.js @@ -0,0 +1,568 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/User/UserController.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const MockResponse = require('../helpers/MockResponse') +const MockRequest = require('../helpers/MockRequest') +const { ObjectId } = require('mongojs') +const assert = require('assert') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('UserController', function() { + beforeEach(function() { + this.user_id = '323123' + + this.user = { + _id: this.user_id, + save: sinon.stub().callsArgWith(0), + ace: {} + } + + this.req = { + user: {}, + session: { + destroy() {}, + user: { + _id: this.user_id, + email: 'old@something.com' + } + }, + body: {} + } + + this.UserDeleter = { deleteUser: sinon.stub().callsArgWith(1) } + this.UserGetter = { getUser: sinon.stub().callsArgWith(1, null, this.user) } + this.User = { findById: sinon.stub().callsArgWith(1, null, this.user) } + this.NewsLetterManager = { unsubscribe: sinon.stub().callsArgWith(1) } + this.UserRegistrationHandler = { registerNewUser: sinon.stub() } + this.AuthenticationController = { + establishUserSession: sinon.stub().callsArg(2), + getLoggedInUserId: sinon.stub().returns(this.user._id), + getSessionUser: sinon.stub().returns(this.req.session.user), + setInSessionUser: sinon.stub() + } + this.AuthenticationManager = { + authenticate: sinon.stub(), + setUserPassword: sinon.stub(), + validatePassword: sinon.stub() + } + this.ReferalAllocator = { allocate: sinon.stub() } + this.SubscriptionDomainHandler = { autoAllocate: sinon.stub() } + this.UserUpdater = { changeEmailAddress: sinon.stub() } + this.settings = { siteUrl: 'sharelatex.example.com' } + this.UserHandler = { populateTeamInvites: sinon.stub().callsArgWith(1) } + this.UserSessionsManager = { + trackSession: sinon.stub(), + untrackSession: sinon.stub(), + revokeAllUserSessions: sinon.stub().callsArgWith(2, null) + } + this.SudoModeHandler = { clearSudoMode: sinon.stub() } + this.UserController = SandboxedModule.require(modulePath, { + requires: { + './UserGetter': this.UserGetter, + './UserDeleter': this.UserDeleter, + './UserUpdater': this.UserUpdater, + '../../models/User': { + User: this.User + }, + '../Newsletter/NewsletterManager': this.NewsLetterManager, + './UserRegistrationHandler': this.UserRegistrationHandler, + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../Authentication/AuthenticationManager': this.AuthenticationManager, + '../Referal/ReferalAllocator': this.ReferalAllocator, + '../Subscription/SubscriptionDomainHandler': this + .SubscriptionDomainHandler, + './UserHandler': this.UserHandler, + './UserSessionsManager': this.UserSessionsManager, + '../SudoMode/SudoModeHandler': this.SudoModeHandler, + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {}, + err() {} + }, + 'metrics-sharelatex': { + inc() {} + }, + '../Errors/Errors': Errors + } + }) + + this.res = { + send: sinon.stub(), + sendStatus: sinon.stub(), + json: sinon.stub() + } + return (this.next = sinon.stub()) + }) + + describe('tryDeleteUser', function() { + beforeEach(function() { + this.req.body.password = 'wat' + this.req.logout = sinon.stub() + this.req.session.destroy = sinon.stub().callsArgWith(0, null) + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.user._id) + this.AuthenticationManager.authenticate = sinon + .stub() + .callsArgWith(2, null, this.user) + return (this.UserDeleter.deleteUser = sinon.stub().callsArgWith(1, null)) + }) + + it('should send 200', function(done) { + this.res.sendStatus = code => { + code.should.equal(200) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + + it('should try to authenticate user', function(done) { + this.res.sendStatus = code => { + this.AuthenticationManager.authenticate.callCount.should.equal(1) + this.AuthenticationManager.authenticate + .calledWith({ _id: this.user._id }, this.req.body.password) + .should.equal(true) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + + it('should delete the user', function(done) { + this.res.sendStatus = code => { + this.UserDeleter.deleteUser.callCount.should.equal(1) + this.UserDeleter.deleteUser.calledWith(this.user._id).should.equal(true) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + + describe('when no password is supplied', function() { + beforeEach(function() { + return (this.req.body.password = '') + }) + + return it('should return 403', function(done) { + this.res.sendStatus = code => { + code.should.equal(403) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + }) + + describe('when authenticate produces an error', function() { + beforeEach(function() { + return (this.AuthenticationManager.authenticate = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + return it('should call next with an error', function(done) { + this.next = err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + }) + + describe('when authenticate does not produce a user', function() { + beforeEach(function() { + return (this.AuthenticationManager.authenticate = sinon + .stub() + .callsArgWith(2, null, null)) + }) + + return it('should return 403', function(done) { + this.res.sendStatus = code => { + code.should.equal(403) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + }) + + describe('when deleteUser produces an error', function() { + beforeEach(function() { + return (this.UserDeleter.deleteUser = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + return it('should call next with an error', function(done) { + this.next = err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + }) + + describe('when deleteUser produces a known error', function() { + beforeEach(function() { + return (this.UserDeleter.deleteUser = sinon + .stub() + .yields(new Errors.SubscriptionAdminDeletionError())) + }) + + return it('should return a json error', function(done) { + return this.UserController.tryDeleteUser(this.req, { + status(status) { + expect(status).to.equal(422) + return { + json(json) { + expect(json.error).to.equal( + Errors.SubscriptionAdminDeletionError.name + ) + return done() + } + } + } + }) + }) + }) + + return describe('when session.destroy produces an error', function() { + beforeEach(function() { + return (this.req.session.destroy = sinon + .stub() + .callsArgWith(0, new Error('woops'))) + }) + + return it('should call next with an error', function(done) { + this.next = err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + } + return this.UserController.tryDeleteUser(this.req, this.res, this.next) + }) + }) + }) + + describe('unsubscribe', () => + it('should send the user to unsubscribe', function(done) { + this.res.send = code => { + this.NewsLetterManager.unsubscribe + .calledWith(this.user) + .should.equal(true) + return done() + } + return this.UserController.unsubscribe(this.req, this.res) + })) + + describe('updateUserSettings', function() { + beforeEach(function() { + this.newEmail = 'hello@world.com' + return (this.req.externalAuthenticationSystemUsed = sinon + .stub() + .returns(false)) + }) + + it('should call save', function(done) { + this.req.body = {} + this.res.sendStatus = code => { + this.user.save.called.should.equal(true) + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should set the first name', function(done) { + this.req.body = { first_name: 'bobby ' } + this.res.sendStatus = code => { + this.user.first_name.should.equal('bobby') + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should set the role', function(done) { + this.req.body = { role: 'student' } + this.res.sendStatus = code => { + this.user.role.should.equal('student') + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should set the institution', function(done) { + this.req.body = { institution: 'MIT' } + this.res.sendStatus = code => { + this.user.institution.should.equal('MIT') + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should set some props on ace', function(done) { + this.req.body = { editorTheme: 'something' } + this.res.sendStatus = code => { + this.user.ace.theme.should.equal('something') + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should set the overall theme', function(done) { + this.req.body = { overallTheme: 'green-ish' } + this.res.sendStatus = code => { + this.user.ace.overallTheme.should.equal('green-ish') + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should send an error if the email is 0 len', function(done) { + this.req.body.email = '' + this.res.sendStatus = function(code) { + code.should.equal(400) + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should send an error if the email does not contain an @', function(done) { + this.req.body.email = 'bob at something dot com' + this.res.sendStatus = function(code) { + code.should.equal(400) + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should call the user updater with the new email and user _id', function(done) { + this.req.body.email = this.newEmail.toUpperCase() + this.UserUpdater.changeEmailAddress.callsArgWith(2) + this.res.sendStatus = code => { + code.should.equal(200) + this.UserUpdater.changeEmailAddress + .calledWith(this.user_id, this.newEmail) + .should.equal(true) + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should update the email on the session', function(done) { + this.req.body.email = this.newEmail.toUpperCase() + this.UserUpdater.changeEmailAddress.callsArgWith(2) + let callcount = 0 + this.User.findById = (id, cb) => { + if (++callcount === 2) { + this.user.email = this.newEmail + } + return cb(null, this.user) + } + this.res.sendStatus = code => { + code.should.equal(200) + this.AuthenticationController.setInSessionUser + .calledWith(this.req, { + email: this.newEmail, + first_name: undefined, + last_name: undefined + }) + .should.equal(true) + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + it('should call populateTeamInvites', function(done) { + this.req.body.email = this.newEmail.toUpperCase() + this.UserUpdater.changeEmailAddress.callsArgWith(2) + this.res.sendStatus = code => { + code.should.equal(200) + this.UserHandler.populateTeamInvites + .calledWith(this.user) + .should.equal(true) + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + + return describe('when using an external auth source', function() { + beforeEach(function() { + this.UserUpdater.changeEmailAddress.callsArgWith(2) + this.newEmail = 'someone23@example.com' + return (this.req.externalAuthenticationSystemUsed = sinon + .stub() + .returns(true)) + }) + + return it('should not set a new email', function(done) { + this.req.body.email = this.newEmail + this.res.sendStatus = code => { + code.should.equal(200) + this.UserUpdater.changeEmailAddress + .calledWith(this.user_id, this.newEmail) + .should.equal(false) + return done() + } + return this.UserController.updateUserSettings(this.req, this.res) + }) + }) + }) + + describe('logout', function() { + it('should destroy the session', function(done) { + this.req.session.destroy = sinon.stub().callsArgWith(0) + this.res.redirect = url => { + url.should.equal('/login') + this.req.session.destroy.called.should.equal(true) + return done() + } + + return this.UserController.logout(this.req, this.res) + }) + + return it('should clear sudo-mode', function(done) { + this.req.session.destroy = sinon.stub().callsArgWith(0) + this.SudoModeHandler.clearSudoMode = sinon.stub() + this.res.redirect = url => { + url.should.equal('/login') + this.SudoModeHandler.clearSudoMode.callCount.should.equal(1) + this.SudoModeHandler.clearSudoMode + .calledWith(this.user._id) + .should.equal(true) + return done() + } + + return this.UserController.logout(this.req, this.res) + }) + }) + + describe('register', function() { + beforeEach(function() { + this.UserRegistrationHandler.registerNewUserAndSendActivationEmail = sinon + .stub() + .callsArgWith(1, null, this.user, (this.url = 'mock/url')) + this.req.body.email = this.user.email = this.email = 'email@example.com' + return this.UserController.register(this.req, this.res) + }) + + it('should register the user and send them an email', function() { + return this.UserRegistrationHandler.registerNewUserAndSendActivationEmail + .calledWith(this.email) + .should.equal(true) + }) + + return it('should return the user and activation url', function() { + return this.res.json + .calledWith({ + email: this.email, + setNewPasswordUrl: this.url + }) + .should.equal(true) + }) + }) + + describe('clearSessions', function() { + it('should call revokeAllUserSessions', function(done) { + this.UserController.clearSessions(this.req, this.res) + this.UserSessionsManager.revokeAllUserSessions.callCount.should.equal(1) + return done() + }) + + it('send a 201 response', function(done) { + this.res.sendStatus = status => { + status.should.equal(201) + return done() + } + return this.UserController.clearSessions(this.req, this.res) + }) + + return describe('when revokeAllUserSessions produces an error', () => + it('should call next with an error', function(done) { + this.UserSessionsManager.revokeAllUserSessions.callsArgWith( + 2, + new Error('woops') + ) + const next = err => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + } + return this.UserController.clearSessions(this.req, this.res, next) + })) + }) + + return describe('changePassword', function() { + it('should check the old password is the current one at the moment', function(done) { + this.AuthenticationManager.authenticate.callsArgWith(2) + this.req.body = { currentPassword: 'oldpasshere' } + this.res.send = () => { + this.AuthenticationManager.authenticate + .calledWith({ _id: this.user._id }, 'oldpasshere') + .should.equal(true) + this.AuthenticationManager.setUserPassword.called.should.equal(false) + return done() + } + return this.UserController.changePassword(this.req, this.res) + }) + + it('it should not set the new password if they do not match', function(done) { + this.AuthenticationManager.authenticate.callsArgWith(2, null, {}) + this.req.body = { + newPassword1: '1', + newPassword2: '2' + } + this.res.send = () => { + this.AuthenticationManager.setUserPassword.called.should.equal(false) + return done() + } + return this.UserController.changePassword(this.req, this.res) + }) + + it('should set the new password if they do match', function(done) { + this.AuthenticationManager.authenticate.callsArgWith(2, null, this.user) + this.AuthenticationManager.setUserPassword.callsArgWith(2) + this.req.body = { + newPassword1: 'newpass', + newPassword2: 'newpass' + } + this.res.send = () => { + this.AuthenticationManager.setUserPassword + .calledWith(this.user._id, 'newpass') + .should.equal(true) + return done() + } + return this.UserController.changePassword(this.req, this.res) + }) + + return it('it should not set the new password if it is invalid', function(done) { + this.AuthenticationManager.validatePassword = sinon + .stub() + .returns({ message: 'password contains invalid characters' }) + this.AuthenticationManager.authenticate.callsArgWith(2, null, {}) + this.req.body = { + newPassword1: 'correct horse battery staple', + newPassword2: 'correct horse battery staple' + } + this.res.send = () => { + this.AuthenticationManager.setUserPassword.called.should.equal(false) + return done() + } + return this.UserController.changePassword(this.req, this.res) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserCreatorTests.js b/services/web/test/unit/src/User/UserCreatorTests.js new file mode 100644 index 0000000000..afe9adbdc9 --- /dev/null +++ b/services/web/test/unit/src/User/UserCreatorTests.js @@ -0,0 +1,151 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const assert = require('assert') +const should = chai.should() +const modulePath = '../../../../app/src/Features/User/UserCreator.js' +const SandboxedModule = require('sandboxed-module') + +describe('UserCreator', function() { + beforeEach(function() { + let Project + const self = this + this.user = { _id: '12390i', ace: {} } + this.user.save = sinon.stub().callsArgWith(0) + this.UserModel = Project = class Project { + constructor() { + return self.user + } + } + + this.UserGetter = { getUserByMainEmail: sinon.stub() } + this.addAffiliation = sinon.stub().yields() + this.UserCreator = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: this.UserModel + }, + 'logger-sharelatex': { log: sinon.stub(), err: sinon.stub() }, + 'metrics-sharelatex': { timeAsyncMethod() {} }, + '../Institutions/InstitutionsAPI': { + addAffiliation: this.addAffiliation + } + } + }) + + return (this.email = 'bob.oswald@gmail.com') + }) + + return describe('createNewUser', function() { + it('should take the opts and put them in the model', function(done) { + const opts = { + email: this.email, + holdingAccount: true + } + return this.UserCreator.createNewUser(opts, (err, user) => { + assert.equal(user.email, this.email) + assert.equal(user.holdingAccount, true) + assert.equal(user.first_name, 'bob.oswald') + return done() + }) + }) + + it('should use the start of the email if the first name is empty string', function(done) { + const opts = { + email: this.email, + holdingAccount: true, + first_name: '' + } + return this.UserCreator.createNewUser(opts, (err, user) => { + assert.equal(user.email, this.email) + assert.equal(user.holdingAccount, true) + assert.equal(user.first_name, 'bob.oswald') + return done() + }) + }) + + it('should use the first name if passed', function(done) { + const opts = { + email: this.email, + holdingAccount: true, + first_name: 'fiiirstname' + } + return this.UserCreator.createNewUser(opts, (err, user) => { + assert.equal(user.email, this.email) + assert.equal(user.holdingAccount, true) + assert.equal(user.first_name, 'fiiirstname') + return done() + }) + }) + + it('should use the last name if passed', function(done) { + const opts = { + email: this.email, + holdingAccount: true, + last_name: 'lastNammmmeee' + } + return this.UserCreator.createNewUser(opts, (err, user) => { + assert.equal(user.email, this.email) + assert.equal(user.holdingAccount, true) + assert.equal(user.last_name, 'lastNammmmeee') + return done() + }) + }) + + it('should set emails attribute', function(done) { + return this.UserCreator.createNewUser( + { email: this.email }, + (err, user) => { + user.email.should.equal(this.email) + user.emails.length.should.equal(1) + user.emails[0].email.should.equal(this.email) + user.emails[0].createdAt.should.be.a('date') + user.emails[0].reversedHostname.should.equal('moc.liamg') + return done() + } + ) + }) + + it('should add affiliation in background', function(done) { + return this.UserCreator.createNewUser( + { email: this.email }, + (err, user) => { + // addaffiliation should not be called before the callback but only after + // a tick of the event loop + sinon.assert.notCalled(this.addAffiliation) + return process.nextTick(() => { + sinon.assert.calledWith(this.addAffiliation, user._id, user.email) + return done() + }) + } + ) + }) + + return it('should not add affiliation if skipping', function(done) { + const attributes = { email: this.email } + const options = { skip_affiliation: true } + return this.UserCreator.createNewUser( + attributes, + options, + (err, user) => { + return process.nextTick(() => { + sinon.assert.notCalled(this.addAffiliation) + return done() + }) + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserDeleterTests.js b/services/web/test/unit/src/User/UserDeleterTests.js new file mode 100644 index 0000000000..94ae46e396 --- /dev/null +++ b/services/web/test/unit/src/User/UserDeleterTests.js @@ -0,0 +1,325 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/User/UserDeleter.js' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('UserDeleter', function() { + beforeEach(function() { + this.user = { + _id: '12390i', + email: 'bob@bob.com', + remove: sinon.stub().callsArgWith(0) + } + + this.User = { findById: sinon.stub().callsArgWith(1, null, this.user) } + + this.NewsletterManager = { unsubscribe: sinon.stub().callsArgWith(1) } + + this.ProjectDeleter = { deleteUsersProjects: sinon.stub().callsArgWith(1) } + + this.SubscriptionHandler = { + cancelSubscription: sinon.stub().callsArgWith(1) + } + + this.SubscriptionUpdater = { + removeUserFromAllGroups: sinon.stub().callsArgWith(1) + } + + this.SubscriptionLocator = { + getUsersSubscription: sinon.stub().yields(null, null) + } + + this.UserMembershipsHandler = { + removeUserFromAllEntities: sinon.stub().callsArgWith(1) + } + + this.deleteAffiliations = sinon.stub().callsArgWith(1) + + this.mongojs = { + db: { + deletedUsers: { + insert: sinon.stub().callsArg(1) + }, + usersDeletedByMigration: { + insert: sinon.stub().callsArg(1) + } + } + } + + return (this.UserDeleter = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { + User: this.User + }, + '../Newsletter/NewsletterManager': this.NewsletterManager, + '../Subscription/SubscriptionHandler': this.SubscriptionHandler, + '../Subscription/SubscriptionUpdater': this.SubscriptionUpdater, + '../Subscription/SubscriptionLocator': this.SubscriptionLocator, + '../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler, + '../Project/ProjectDeleter': this.ProjectDeleter, + '../Institutions/InstitutionsAPI': { + deleteAffiliations: this.deleteAffiliations + }, + '../../infrastructure/mongojs': this.mongojs, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }), + '../Errors/Errors': Errors + } + })) + }) + + describe('softDeleteUserForMigration', function() { + beforeEach(function() { + return (this.UserDeleter._ensureCanDeleteUser = sinon.stub().yields(null)) + }) + + it('should delete the user in mongo', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + this.User.findById.calledWith(this.user._id).should.equal(true) + this.user.remove.called.should.equal(true) + return done() + }) + }) + + it('should add the user to the deletedUsers collection', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + sinon.assert.calledWith( + this.mongojs.db.usersDeletedByMigration.insert, + this.user + ) + return done() + }) + }) + + it('should set the deletedAt field on the user', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + this.user.deletedAt.should.exist + return done() + }) + }) + + it('should unsubscribe the user from the news letter', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + this.NewsletterManager.unsubscribe + .calledWith(this.user) + .should.equal(true) + return done() + }) + }) + + it('should unsubscribe the user', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + this.SubscriptionHandler.cancelSubscription + .calledWith(this.user) + .should.equal(true) + return done() + }) + }) + + it('should delete user affiliations', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + this.deleteAffiliations.calledWith(this.user._id).should.equal(true) + return done() + }) + }) + + it('should delete all the projects of a user', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + this.ProjectDeleter.deleteUsersProjects + .calledWith(this.user._id) + .should.equal(true) + return done() + }) + }) + + it('should remove user memberships', function(done) { + return this.UserDeleter.softDeleteUserForMigration(this.user._id, err => { + this.UserMembershipsHandler.removeUserFromAllEntities + .calledWith(this.user._id) + .should.equal(true) + return done() + }) + }) + + return it('ensures user can be deleted first', function(done) { + this.UserDeleter._ensureCanDeleteUser.yields( + new Errors.SubscriptionAdminDeletionError() + ) + return this.UserDeleter.softDeleteUserForMigration( + this.user._id, + error => { + sinon.assert.calledWith( + this.UserDeleter._ensureCanDeleteUser, + this.user + ) + sinon.assert.notCalled(this.user.remove) + expect(error).to.be.instanceof(Errors.SubscriptionAdminDeletionError) + return done() + } + ) + }) + }) + + describe('deleteUser', function() { + beforeEach(function() { + return (this.UserDeleter._ensureCanDeleteUser = sinon.stub().yields(null)) + }) + + it('should delete the user in mongo', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.User.findById.calledWith(this.user._id).should.equal(true) + this.user.remove.called.should.equal(true) + return done() + }) + }) + + it('should unsubscribe the user from the news letter', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.NewsletterManager.unsubscribe + .calledWith(this.user) + .should.equal(true) + return done() + }) + }) + + it('should delete all the projects of a user', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.ProjectDeleter.deleteUsersProjects + .calledWith(this.user._id) + .should.equal(true) + return done() + }) + }) + + it('should unsubscribe the user', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.SubscriptionHandler.cancelSubscription + .calledWith(this.user) + .should.equal(true) + return done() + }) + }) + + it('should delete user affiliations', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.deleteAffiliations.calledWith(this.user._id).should.equal(true) + return done() + }) + }) + + it('should remove user from group subscriptions', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.SubscriptionUpdater.removeUserFromAllGroups + .calledWith(this.user._id) + .should.equal(true) + return done() + }) + }) + + it('should remove user memberships', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.UserMembershipsHandler.removeUserFromAllEntities + .calledWith(this.user._id) + .should.equal(true) + return done() + }) + }) + + it('ensures user can be deleted first', function(done) { + this.UserDeleter._ensureCanDeleteUser.yields( + new Errors.SubscriptionAdminDeletionError() + ) + return this.UserDeleter.deleteUser(this.user._id, error => { + sinon.assert.calledWith( + this.UserDeleter._ensureCanDeleteUser, + this.user + ) + sinon.assert.notCalled(this.user.remove) + expect(error).to.be.instanceof(Errors.SubscriptionAdminDeletionError) + return done() + }) + }) + + return describe('when unsubscribing from mailchimp fails', function() { + beforeEach(function() { + return (this.NewsletterManager.unsubscribe = sinon + .stub() + .callsArgWith(1, new Error('something went wrong'))) + }) + + it('should not return an error', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.NewsletterManager.unsubscribe + .calledWith(this.user) + .should.equal(true) + should.not.exist(err) + return done() + }) + }) + + it('should delete the user', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + this.NewsletterManager.unsubscribe + .calledWith(this.user) + .should.equal(true) + this.user.remove.called.should.equal(true) + return done() + }) + }) + + return it('should log an error', function(done) { + return this.UserDeleter.deleteUser(this.user._id, err => { + sinon.assert.called(this.logger.err) + return done() + }) + }) + }) + }) + + return describe('_ensureCanDeleteUser', function() { + it('should not return error when user can be deleted', function(done) { + this.SubscriptionLocator.getUsersSubscription.yields(null, null) + return this.UserDeleter._ensureCanDeleteUser(this.user, function(error) { + expect(error).to.not.exist + return done() + }) + }) + + it('should return custom error when user is group admin', function(done) { + this.SubscriptionLocator.getUsersSubscription.yields(null, { + _id: '123abc' + }) + return this.UserDeleter._ensureCanDeleteUser(this.user, function(error) { + expect(error).to.be.instanceof(Errors.SubscriptionAdminDeletionError) + return done() + }) + }) + + return it('propagate errors', function(done) { + this.SubscriptionLocator.getUsersSubscription.yields( + new Error('Some error') + ) + return this.UserDeleter._ensureCanDeleteUser(this.user, function(error) { + expect(error).to.be.instanceof(Error) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserEmailsConfirmationHandlerTests.js b/services/web/test/unit/src/User/UserEmailsConfirmationHandlerTests.js new file mode 100644 index 0000000000..d221ddc4be --- /dev/null +++ b/services/web/test/unit/src/User/UserEmailsConfirmationHandlerTests.js @@ -0,0 +1,230 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/User/UserEmailsConfirmationHandler' +) +const { expect } = require('chai') +const Errors = require('../../../../app/src/Features/Errors/Errors') +const EmailHelper = require('../../../../app/src/Features/Helpers/EmailHelper') + +describe('UserEmailsConfirmationHandler', function() { + beforeEach(function() { + this.UserEmailsConfirmationHandler = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = { + siteUrl: 'emails.example.com' + }), + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + '../Security/OneTimeTokenHandler': (this.OneTimeTokenHandler = {}), + '../Errors/Errors': Errors, + './UserUpdater': (this.UserUpdater = {}), + './UserGetter': (this.UserGetter = { + getUser: sinon.stub().yields(null, this.mockUser) + }), + '../Email/EmailHandler': (this.EmailHandler = {}), + '../Helpers/EmailHelper': EmailHelper + } + }) + this.mockUser = { _id: 'mock-user-id' } + this.user_id = this.mockUser._id + this.email = 'mock@example.com' + return (this.callback = sinon.stub()) + }) + + describe('sendConfirmationEmail', function() { + beforeEach(function() { + this.OneTimeTokenHandler.getNewToken = sinon + .stub() + .yields(null, (this.token = 'new-token')) + return (this.EmailHandler.sendEmail = sinon.stub().yields()) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.UserEmailsConfirmationHandler.sendConfirmationEmail( + this.user_id, + this.email, + this.callback + ) + }) + + it('should generate a token for the user which references their id and email', function() { + return this.OneTimeTokenHandler.getNewToken + .calledWith( + 'email_confirmation', + { user_id: this.user_id, email: this.email }, + { expiresIn: 365 * 24 * 60 * 60 } + ) + .should.equal(true) + }) + + it('should send an email to the user', function() { + return this.EmailHandler.sendEmail + .calledWith('confirmEmail', { + to: this.email, + confirmEmailUrl: + 'emails.example.com/user/emails/confirm?token=new-token', + sendingUser_id: this.user_id + }) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('with invalid email', function() { + beforeEach(function() { + return this.UserEmailsConfirmationHandler.sendConfirmationEmail( + this.user_id, + '!"£$%^&*()', + this.callback + ) + }) + + return it('should return an error', function() { + return this.callback + .calledWith(sinon.match.instanceOf(Error)) + .should.equal(true) + }) + }) + + return describe('a custom template', function() { + beforeEach(function() { + return this.UserEmailsConfirmationHandler.sendConfirmationEmail( + this.user_id, + this.email, + 'myCustomTemplate', + this.callback + ) + }) + + return it('should send an email with the given template', function() { + return this.EmailHandler.sendEmail + .calledWith('myCustomTemplate') + .should.equal(true) + }) + }) + }) + + return describe('confirmEmailFromToken', function() { + beforeEach(function() { + this.OneTimeTokenHandler.getValueFromTokenAndExpire = sinon + .stub() + .yields(null, { user_id: this.user_id, email: this.email }) + return (this.UserUpdater.confirmEmail = sinon.stub().yields()) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.UserEmailsConfirmationHandler.confirmEmailFromToken( + (this.token = 'mock-token'), + this.callback + ) + }) + + it('should call getValueFromTokenAndExpire', function() { + return this.OneTimeTokenHandler.getValueFromTokenAndExpire + .calledWith('email_confirmation', this.token) + .should.equal(true) + }) + + it('should confirm the email of the user_id', function() { + return this.UserUpdater.confirmEmail + .calledWith(this.user_id, this.email) + .should.equal(true) + }) + + return it('should call the callback', function() { + return this.callback.called.should.equal(true) + }) + }) + + describe('with an expired token', function() { + beforeEach(function() { + this.OneTimeTokenHandler.getValueFromTokenAndExpire = sinon + .stub() + .yields(null, null) + return this.UserEmailsConfirmationHandler.confirmEmailFromToken( + (this.token = 'mock-token'), + this.callback + ) + }) + + return it('should call the callback with a NotFoundError', function() { + return this.callback + .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) + .should.equal(true) + }) + }) + + describe('with no user_id in the token', function() { + beforeEach(function() { + this.OneTimeTokenHandler.getValueFromTokenAndExpire = sinon + .stub() + .yields(null, { email: this.email }) + return this.UserEmailsConfirmationHandler.confirmEmailFromToken( + (this.token = 'mock-token'), + this.callback + ) + }) + + return it('should call the callback with a NotFoundError', function() { + return this.callback + .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) + .should.equal(true) + }) + }) + + describe('with no email in the token', function() { + beforeEach(function() { + this.OneTimeTokenHandler.getValueFromTokenAndExpire = sinon + .stub() + .yields(null, { user_id: this.user_id }) + return this.UserEmailsConfirmationHandler.confirmEmailFromToken( + (this.token = 'mock-token'), + this.callback + ) + }) + + return it('should call the callback with a NotFoundError', function() { + return this.callback + .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) + .should.equal(true) + }) + }) + + return describe('with no user found', function() { + beforeEach(function() { + this.UserGetter.getUser.yields(null, null) + return this.UserEmailsConfirmationHandler.confirmEmailFromToken( + (this.token = 'mock-token'), + this.callback + ) + }) + + return it('should call the callback with a NotFoundError', function() { + return this.callback + .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserEmailsControllerTests.js b/services/web/test/unit/src/User/UserEmailsControllerTests.js new file mode 100644 index 0000000000..8c115cd183 --- /dev/null +++ b/services/web/test/unit/src/User/UserEmailsControllerTests.js @@ -0,0 +1,308 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const assertCalledWith = sinon.assert.calledWith +const assertNotCalled = sinon.assert.notCalled +const chai = require('chai') +const should = chai.should() +const { assert } = chai +const modulePath = '../../../../app/src/Features/User/UserEmailsController.js' +const SandboxedModule = require('sandboxed-module') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('UserEmailsController', function() { + beforeEach(function() { + this.req = new MockRequest() + this.user = { _id: 'mock-user-id' } + + this.UserGetter = { getUserFullEmails: sinon.stub() } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user._id) + } + this.UserUpdater = { + addEmailAddress: sinon.stub(), + removeEmailAddress: sinon.stub(), + setDefaultEmailAddress: sinon.stub(), + updateV1AndSetDefaultEmailAddress: sinon.stub() + } + this.EmailHelper = { parseEmail: sinon.stub() } + this.endorseAffiliation = sinon.stub().yields() + return (this.UserEmailsController = SandboxedModule.require(modulePath, { + requires: { + '../Authentication/AuthenticationController': this + .AuthenticationController, + './UserGetter': this.UserGetter, + './UserUpdater': this.UserUpdater, + '../Helpers/EmailHelper': this.EmailHelper, + './UserEmailsConfirmationHandler': (this.UserEmailsConfirmationHandler = {}), + '../Institutions/InstitutionsAPI': { + endorseAffiliation: this.endorseAffiliation + }, + '../Errors/Errors': Errors, + 'logger-sharelatex': { + log() { + return console.log(arguments) + }, + err() {} + } + } + })) + }) + + describe('List', function() { + beforeEach(function() {}) + + return it('lists emails', function(done) { + const fullEmails = [{ some: 'data' }] + this.UserGetter.getUserFullEmails.callsArgWith(1, null, fullEmails) + + return this.UserEmailsController.list(this.req, { + json: response => { + assert.deepEqual(response, fullEmails) + assertCalledWith(this.UserGetter.getUserFullEmails, this.user._id) + return done() + } + }) + }) + }) + + describe('Add', function() { + beforeEach(function() { + this.newEmail = 'new_email@baz.com' + this.req.body = { + email: this.newEmail, + university: { name: 'University Name' }, + department: 'Department', + role: 'Role' + } + this.EmailHelper.parseEmail.returns(this.newEmail) + this.UserEmailsConfirmationHandler.sendConfirmationEmail = sinon + .stub() + .yields() + return this.UserUpdater.addEmailAddress.callsArgWith(3, null) + }) + + it('adds new email', function(done) { + return this.UserEmailsController.add(this.req, { + sendStatus: code => { + code.should.equal(204) + assertCalledWith(this.EmailHelper.parseEmail, this.newEmail) + assertCalledWith( + this.UserUpdater.addEmailAddress, + this.user._id, + this.newEmail + ) + + const affiliationOptions = this.UserUpdater.addEmailAddress.lastCall + .args[2] + Object.keys(affiliationOptions).length.should.equal(3) + affiliationOptions.university.should.equal(this.req.body.university) + affiliationOptions.department.should.equal(this.req.body.department) + affiliationOptions.role.should.equal(this.req.body.role) + + return done() + } + }) + }) + + it('sends an email confirmation', function(done) { + return this.UserEmailsController.add(this.req, { + sendStatus: code => { + code.should.equal(204) + assertCalledWith( + this.UserEmailsConfirmationHandler.sendConfirmationEmail, + this.user._id, + this.newEmail + ) + return done() + } + }) + }) + + return it('handles email parse error', function(done) { + this.EmailHelper.parseEmail.returns(null) + return this.UserEmailsController.add(this.req, { + sendStatus: code => { + code.should.equal(422) + assertNotCalled(this.UserUpdater.addEmailAddress) + return done() + } + }) + }) + }) + + describe('remove', function() { + beforeEach(function() { + this.email = 'email_to_remove@bar.com' + this.req.body.email = this.email + return this.EmailHelper.parseEmail.returns(this.email) + }) + + it('removes email', function(done) { + this.UserUpdater.removeEmailAddress.callsArgWith(2, null) + + return this.UserEmailsController.remove(this.req, { + sendStatus: code => { + code.should.equal(200) + assertCalledWith(this.EmailHelper.parseEmail, this.email) + assertCalledWith( + this.UserUpdater.removeEmailAddress, + this.user._id, + this.email + ) + return done() + } + }) + }) + + return it('handles email parse error', function(done) { + this.EmailHelper.parseEmail.returns(null) + + return this.UserEmailsController.remove(this.req, { + sendStatus: code => { + code.should.equal(422) + assertNotCalled(this.UserUpdater.removeEmailAddress) + return done() + } + }) + }) + }) + + describe('setDefault', function() { + beforeEach(function() { + this.email = 'email_to_set_default@bar.com' + this.req.body.email = this.email + return this.EmailHelper.parseEmail.returns(this.email) + }) + + it('sets default email', function(done) { + this.UserUpdater.updateV1AndSetDefaultEmailAddress.callsArgWith(2, null) + + return this.UserEmailsController.setDefault(this.req, { + sendStatus: code => { + code.should.equal(200) + assertCalledWith(this.EmailHelper.parseEmail, this.email) + assertCalledWith( + this.UserUpdater.updateV1AndSetDefaultEmailAddress, + this.user._id, + this.email + ) + return done() + } + }) + }) + + return it('handles email parse error', function(done) { + this.EmailHelper.parseEmail.returns(null) + + return this.UserEmailsController.setDefault(this.req, { + sendStatus: code => { + code.should.equal(422) + assertNotCalled(this.UserUpdater.setDefaultEmailAddress) + return done() + } + }) + }) + }) + + describe('endorse', function() { + beforeEach(function() { + this.email = 'email_to_endorse@bar.com' + this.req.body.email = this.email + return this.EmailHelper.parseEmail.returns(this.email) + }) + + return it('endorses affiliation', function(done) { + this.req.body.role = 'Role' + this.req.body.department = 'Department' + + return this.UserEmailsController.endorse(this.req, { + sendStatus: code => { + code.should.equal(204) + assertCalledWith( + this.endorseAffiliation, + this.user._id, + this.email, + 'Role', + 'Department' + ) + return done() + } + }) + }) + }) + + return describe('confirm', function() { + beforeEach(function() { + this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon + .stub() + .yields() + this.res = { + sendStatus: sinon.stub(), + json: sinon.stub() + } + this.res.status = sinon.stub().returns(this.res) + this.next = sinon.stub() + this.token = 'mock-token' + return (this.req.body = { token: this.token }) + }) + + describe('successfully', function() { + beforeEach(function() { + return this.UserEmailsController.confirm(this.req, this.res, this.next) + }) + + it('should confirm the email from the token', function() { + return this.UserEmailsConfirmationHandler.confirmEmailFromToken + .calledWith(this.token) + .should.equal(true) + }) + + return it('should return a 200 status', function() { + return this.res.sendStatus.calledWith(200).should.equal(true) + }) + }) + + describe('without a token', function() { + beforeEach(function() { + this.req.body.token = null + return this.UserEmailsController.confirm(this.req, this.res, this.next) + }) + + return it('should return a 422 status', function() { + return this.res.sendStatus.calledWith(422).should.equal(true) + }) + }) + + return describe('when confirming fails', function() { + beforeEach(function() { + this.UserEmailsConfirmationHandler.confirmEmailFromToken = sinon + .stub() + .yields(new Errors.NotFoundError('not found')) + return this.UserEmailsController.confirm(this.req, this.res, this.next) + }) + + return it('should return a 404 error code with a message', function() { + this.res.status.calledWith(404).should.equal(true) + return this.res.json + .calledWith({ + message: + 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.' + }) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserGetterTests.js b/services/web/test/unit/src/User/UserGetterTests.js new file mode 100644 index 0000000000..0d5a351a56 --- /dev/null +++ b/services/web/test/unit/src/User/UserGetterTests.js @@ -0,0 +1,328 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { ObjectId } = require('mongojs') +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/User/UserGetter' +) +const { expect } = require('chai') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('UserGetter', function() { + beforeEach(function() { + this.fakeUser = { + _id: '12390i', + email: 'email2@foo.bar', + emails: [ + { email: 'email1@foo.bar', reversedHostname: 'rab.oof' }, + { email: 'email2@foo.bar', reversedHostname: 'rab.oof' } + ] + } + this.findOne = sinon.stub().callsArgWith(2, null, this.fakeUser) + this.find = sinon.stub().callsArgWith(2, null, [this.fakeUser]) + this.Mongo = { + db: { + users: { + findOne: this.findOne, + find: this.find + } + }, + ObjectId + } + const settings = { apis: { v1: { url: 'v1.url', user: '', pass: '' } } } + this.getUserAffiliations = sinon.stub().callsArgWith(1, null, []) + + return (this.UserGetter = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {} + }, + '../../infrastructure/mongojs': this.Mongo, + 'metrics-sharelatex': { + timeAsyncMethod: sinon.stub() + }, + 'settings-sharelatex': settings, + '../Institutions/InstitutionsAPI': { + getUserAffiliations: this.getUserAffiliations + }, + '../Errors/Errors': Errors + } + })) + }) + + describe('getUser', function() { + it('should get user', function(done) { + const query = { _id: 'foo' } + const projection = { email: 1 } + return this.UserGetter.getUser(query, projection, (error, user) => { + this.findOne.called.should.equal(true) + this.findOne.calledWith(query, projection).should.equal(true) + user.should.deep.equal(this.fakeUser) + return done() + }) + }) + + it('should not allow email in query', function(done) { + return this.UserGetter.getUser( + { email: 'foo@bar.com' }, + {}, + (error, user) => { + error.should.exist + return done() + } + ) + }) + + return it('should not allow null query', function(done) { + return this.UserGetter.getUser(null, {}, (error, user) => { + error.should.exist + return done() + }) + }) + }) + + describe('getUserFullEmails', function() { + it('should get user', function(done) { + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, this.fakeUser) + const projection = { email: 1, emails: 1 } + return this.UserGetter.getUserFullEmails( + this.fakeUser._id, + (error, fullEmails) => { + this.UserGetter.getUser.called.should.equal(true) + this.UserGetter.getUser + .calledWith(this.fakeUser._id, projection) + .should.equal(true) + return done() + } + ) + }) + + it('should fetch emails data', function(done) { + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, this.fakeUser) + return this.UserGetter.getUserFullEmails( + this.fakeUser._id, + (error, fullEmails) => { + assert.deepEqual(fullEmails, [ + { + email: 'email1@foo.bar', + reversedHostname: 'rab.oof', + default: false + }, + { + email: 'email2@foo.bar', + reversedHostname: 'rab.oof', + default: true + } + ]) + return done() + } + ) + }) + + it('should merge affiliation data', function(done) { + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, this.fakeUser) + const affiliationsData = [ + { + email: 'email1@foo.bar', + role: 'Prof', + department: 'Maths', + inferred: false, + institution: { name: 'University Name', isUniversity: true } + } + ] + this.getUserAffiliations.callsArgWith(1, null, affiliationsData) + return this.UserGetter.getUserFullEmails( + this.fakeUser._id, + (error, fullEmails) => { + assert.deepEqual(fullEmails, [ + { + email: 'email1@foo.bar', + reversedHostname: 'rab.oof', + default: false, + affiliation: { + institution: affiliationsData[0].institution, + inferred: affiliationsData[0].inferred, + department: affiliationsData[0].department, + role: affiliationsData[0].role + } + }, + { + email: 'email2@foo.bar', + reversedHostname: 'rab.oof', + default: true + } + ]) + return done() + } + ) + }) + + return it('should get user when it has no emails field', function(done) { + this.fakeUser = { + _id: '12390i', + email: 'email2@foo.bar' + } + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, this.fakeUser) + const projection = { email: 1, emails: 1 } + return this.UserGetter.getUserFullEmails( + this.fakeUser._id, + (error, fullEmails) => { + this.UserGetter.getUser.called.should.equal(true) + this.UserGetter.getUser + .calledWith(this.fakeUser._id, projection) + .should.equal(true) + assert.deepEqual(fullEmails, []) + return done() + } + ) + }) + }) + + describe('getUserbyMainEmail', function() { + it('query user by main email', function(done) { + const email = 'hello@world.com' + const projection = { emails: 1 } + return this.UserGetter.getUserByMainEmail( + email, + projection, + (error, user) => { + this.findOne.called.should.equal(true) + this.findOne.calledWith({ email }, projection).should.equal(true) + return done() + } + ) + }) + + it('return user if found', function(done) { + const email = 'hello@world.com' + return this.UserGetter.getUserByMainEmail(email, (error, user) => { + user.should.deep.equal(this.fakeUser) + return done() + }) + }) + + return it('trim email', function(done) { + const email = 'hello@world.com' + return this.UserGetter.getUserByMainEmail(` ${email} `, (error, user) => { + this.findOne.called.should.equal(true) + this.findOne.calledWith({ email }).should.equal(true) + return done() + }) + }) + }) + + describe('getUserByAnyEmail', function() { + it('query user for any email', function(done) { + const email = 'hello@world.com' + const expectedQuery = { + emails: { $exists: true }, + 'emails.email': email + } + const projection = { emails: 1 } + return this.UserGetter.getUserByAnyEmail( + ` ${email} `, + projection, + (error, user) => { + this.findOne.calledWith(expectedQuery, projection).should.equal(true) + user.should.deep.equal(this.fakeUser) + return done() + } + ) + }) + + it('query contains $exists:true so partial index is used', function(done) { + const expectedQuery = { + emails: { $exists: true }, + 'emails.email': '' + } + return this.UserGetter.getUserByAnyEmail('', {}, (error, user) => { + this.findOne.calledWith(expectedQuery, {}).should.equal(true) + return done() + }) + }) + + return it('checks main email as well', function(done) { + this.findOne.callsArgWith(2, null, null) + const email = 'hello@world.com' + const projection = { emails: 1 } + return this.UserGetter.getUserByAnyEmail( + ` ${email} `, + projection, + (error, user) => { + this.findOne.calledTwice.should.equal(true) + this.findOne.calledWith({ email }, projection).should.equal(true) + return done() + } + ) + }) + }) + + describe('getUsersByHostname', () => + it('should find user by hostname', function(done) { + const hostname = 'bar.foo' + const expectedQuery = { + emails: { $exists: true }, + 'emails.reversedHostname': hostname + .split('') + .reverse() + .join('') + } + const projection = { emails: 1 } + return this.UserGetter.getUsersByHostname( + hostname, + projection, + (error, users) => { + this.find.calledOnce.should.equal(true) + this.find.calledWith(expectedQuery, projection).should.equal(true) + return done() + } + ) + })) + + return describe('ensureUniqueEmailAddress', function() { + beforeEach(function() { + return (this.UserGetter.getUserByAnyEmail = sinon.stub()) + }) + + it('should return error if existing user is found', function(done) { + this.UserGetter.getUserByAnyEmail.callsArgWith(1, null, this.fakeUser) + return this.UserGetter.ensureUniqueEmailAddress(this.newEmail, err => { + should.exist(err) + expect(err).to.be.an.instanceof(Errors.EmailExistsError) + err.message.should.equal('alread_exists') + return done() + }) + }) + + return it('should return null if no user is found', function(done) { + this.UserGetter.getUserByAnyEmail.callsArgWith(1) + return this.UserGetter.ensureUniqueEmailAddress(this.newEmail, err => { + should.not.exist(err) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserHandlerTests.js b/services/web/test/unit/src/User/UserHandlerTests.js new file mode 100644 index 0000000000..52c7444172 --- /dev/null +++ b/services/web/test/unit/src/User/UserHandlerTests.js @@ -0,0 +1,49 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const modulePath = '../../../../app/src/Features/User/UserHandler.js' +const SandboxedModule = require('sandboxed-module') + +describe('UserHandler', function() { + beforeEach(function() { + this.user = { + _id: '12390i', + email: 'bob@bob.com', + remove: sinon.stub().callsArgWith(0) + } + + this.TeamInvitesHandler = { + createTeamInvitesForLegacyInvitedEmail: sinon.stub().yields() + } + + return (this.UserHandler = SandboxedModule.require(modulePath, { + requires: { + '../Subscription/TeamInvitesHandler': this.TeamInvitesHandler + } + })) + }) + + return describe('populateTeamInvites', function() { + beforeEach(function(done) { + return this.UserHandler.populateTeamInvites(this.user, done) + }) + + return it('notifies the user about legacy team invites', function() { + return this.TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail + .calledWith(this.user.email) + .should.eq(true) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserInfoControllerTests.js b/services/web/test/unit/src/User/UserInfoControllerTests.js new file mode 100644 index 0000000000..86cef9dcfc --- /dev/null +++ b/services/web/test/unit/src/User/UserInfoControllerTests.js @@ -0,0 +1,240 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const { assert } = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/User/UserInfoController.js' +const SandboxedModule = require('sandboxed-module') +const events = require('events') +const MockResponse = require('../helpers/MockResponse') +const MockRequest = require('../helpers/MockRequest') +const { ObjectId } = require('mongojs') + +describe('UserInfoController', function() { + beforeEach(function() { + this.UserDeleter = { deleteUser: sinon.stub().callsArgWith(1) } + this.UserUpdater = { updatePersonalInfo: sinon.stub() } + this.sanitizer = { + escape(v) { + return v + } + } + sinon.spy(this.sanitizer, 'escape') + this.UserGetter = {} + + this.UserInfoController = SandboxedModule.require(modulePath, { + requires: { + './UserGetter': this.UserGetter, + './UserUpdater': this.UserUpdater, + './UserDeleter': this.UserDeleter, + 'logger-sharelatex': { + log() {} + }, + sanitizer: this.sanitizer, + '../Authentication/AuthenticationController': (this.AuthenticationController = { + getLoggedInUserId: sinon.stub() + }) + } + }) + + this.req = new MockRequest() + this.res = new MockResponse() + return (this.next = sinon.stub()) + }) + + describe('getLoggedInUsersPersonalInfo', function() { + beforeEach(function() { + this.user = { _id: ObjectId() } + this.req.user = this.user + this.req.session.user = this.user + this.UserInfoController.sendFormattedPersonalInfo = sinon.stub() + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user) + this.AuthenticationController.getLoggedInUserId = sinon + .stub() + .returns(this.user._id) + return this.UserInfoController.getLoggedInUsersPersonalInfo( + this.req, + this.res, + this.next + ) + }) + + return it('should call sendFormattedPersonalInfo', function() { + return this.UserInfoController.sendFormattedPersonalInfo + .calledWith(this.user, this.res, this.next) + .should.equal(true) + }) + }) + + describe('getPersonalInfo', function() { + describe('when the user exists with sharelatex id', function() { + beforeEach(function() { + this.user_id = ObjectId().toString() + this.user = { _id: ObjectId(this.user_id) } + this.req.params = { user_id: this.user_id } + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user) + this.UserInfoController.sendFormattedPersonalInfo = sinon.stub() + return this.UserInfoController.getPersonalInfo( + this.req, + this.res, + this.next + ) + }) + + it('should look up the user in the database', function() { + return this.UserGetter.getUser + .calledWith( + { _id: ObjectId(this.user_id) }, + { _id: true, first_name: true, last_name: true, email: true } + ) + .should.equal(true) + }) + + return it('should send the formatted details back to the client', function() { + return this.UserInfoController.sendFormattedPersonalInfo + .calledWith(this.user, this.res, this.next) + .should.equal(true) + }) + }) + + describe('when the user exists with overleaf id', function() { + beforeEach(function() { + this.user_id = 12345 + this.user = { + _id: ObjectId(), + overleaf: { + id: this.user_id + } + } + this.req.params = { user_id: this.user_id.toString() } + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user) + this.UserInfoController.sendFormattedPersonalInfo = sinon.stub() + return this.UserInfoController.getPersonalInfo( + this.req, + this.res, + this.next + ) + }) + + it('should look up the user in the database', function() { + return this.UserGetter.getUser + .calledWith( + { 'overleaf.id': this.user_id }, + { _id: true, first_name: true, last_name: true, email: true } + ) + .should.equal(true) + }) + + return it('should send the formatted details back to the client', function() { + return this.UserInfoController.sendFormattedPersonalInfo + .calledWith(this.user, this.res, this.next) + .should.equal(true) + }) + }) + + describe('when the user does not exist', function() { + beforeEach(function() { + this.user_id = ObjectId().toString() + this.req.params = { user_id: this.user_id } + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) + return this.UserInfoController.getPersonalInfo( + this.req, + this.res, + this.next + ) + }) + + return it('should return 404 to the client', function() { + return this.res.statusCode.should.equal(404) + }) + }) + + return describe('when the user id is invalid', function() { + beforeEach(function() { + this.user_id = 'invalid' + this.req.params = { user_id: this.user_id } + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) + return this.UserInfoController.getPersonalInfo( + this.req, + this.res, + this.next + ) + }) + + return it('should return 400 to the client', function() { + return this.res.statusCode.should.equal(400) + }) + }) + }) + + describe('sendFormattedPersonalInfo', function() { + beforeEach(function() { + this.user = { + _id: ObjectId(), + first_name: 'Douglas', + last_name: 'Adams', + email: 'doug@sharelatex.com' + } + this.formattedInfo = { + id: this.user._id.toString(), + first_name: this.user.first_name, + last_name: this.user.last_name, + email: this.user.email + } + this.UserInfoController.formatPersonalInfo = sinon + .stub() + .returns(this.formattedInfo) + return this.UserInfoController.sendFormattedPersonalInfo( + this.user, + this.res + ) + }) + + it('should format the user details for the response', function() { + return this.UserInfoController.formatPersonalInfo + .calledWith(this.user) + .should.equal(true) + }) + + return it('should send the formatted details back to the client', function() { + return this.res.body.should.equal(JSON.stringify(this.formattedInfo)) + }) + }) + + return describe('formatPersonalInfo', () => + it('should return the correctly formatted data', function() { + this.user = { + _id: ObjectId(), + first_name: 'Douglas', + last_name: 'Adams', + email: 'doug@sharelatex.com', + password: 'should-not-get-included', + signUpDate: new Date(), + role: 'student', + institution: 'sheffield' + } + return expect( + this.UserInfoController.formatPersonalInfo(this.user) + ).to.deep.equal({ + id: this.user._id.toString(), + first_name: this.user.first_name, + last_name: this.user.last_name, + email: this.user.email, + signUpDate: this.user.signUpDate, + role: this.user.role, + institution: this.user.institution + }) + })) +}) diff --git a/services/web/test/unit/src/User/UserPagesControllerTests.js b/services/web/test/unit/src/User/UserPagesControllerTests.js new file mode 100644 index 0000000000..5bb1420cf1 --- /dev/null +++ b/services/web/test/unit/src/User/UserPagesControllerTests.js @@ -0,0 +1,338 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/User/UserPagesController' +) +const { expect } = require('chai') + +describe('UserPagesController', function() { + beforeEach(function() { + this.settings = { + apis: { + v1: { + url: 'some.host', + user: 'one', + pass: 'two' + } + } + } + this.user = { + _id: (this.user_id = 'kwjewkl'), + features: {}, + email: 'joe@example.com', + thirdPartyIdentifiers: [ + { + providerId: 'google', + externalUserId: 'testId' + } + ] + } + + this.UserGetter = { getUser: sinon.stub() } + this.UserSessionsManager = { getAllUserSessions: sinon.stub() } + this.dropboxStatus = {} + this.DropboxHandler = { + getUserRegistrationStatus: sinon + .stub() + .callsArgWith(1, null, this.dropboxStatus) + } + this.ErrorController = { notFound: sinon.stub() } + this.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(this.user._id), + getSessionUser: sinon.stub().returns(this.user), + _getRedirectFromSession: sinon.stub(), + setRedirectInSession: sinon.stub() + } + this.UserPagesController = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {}, + err() {} + }, + './UserGetter': this.UserGetter, + './UserSessionsManager': this.UserSessionsManager, + '../Errors/ErrorController': this.ErrorController, + '../Dropbox/DropboxHandler': this.DropboxHandler, + '../Authentication/AuthenticationController': this + .AuthenticationController, + request: (this.request = sinon.stub()) + } + }) + this.req = { + query: {}, + session: { + user: this.user + } + } + return (this.res = {}) + }) + + describe('registerPage', function() { + it('should render the register page', function(done) { + this.res.render = page => { + page.should.equal('user/register') + return done() + } + return this.UserPagesController.registerPage(this.req, this.res) + }) + + it('should set sharedProjectData', function(done) { + this.req.query.project_name = 'myProject' + this.req.query.user_first_name = 'user_first_name_here' + + this.res.render = (page, opts) => { + opts.sharedProjectData.project_name.should.equal('myProject') + opts.sharedProjectData.user_first_name.should.equal( + 'user_first_name_here' + ) + return done() + } + return this.UserPagesController.registerPage(this.req, this.res) + }) + + it('should set newTemplateData', function(done) { + this.req.session.templateData = { templateName: 'templateName' } + + this.res.render = (page, opts) => { + opts.newTemplateData.templateName.should.equal('templateName') + return done() + } + return this.UserPagesController.registerPage(this.req, this.res) + }) + + return it('should not set the newTemplateData if there is nothing in the session', function(done) { + this.res.render = (page, opts) => { + assert.equal(opts.newTemplateData.templateName, undefined) + return done() + } + return this.UserPagesController.registerPage(this.req, this.res) + }) + }) + + describe('loginForm', function() { + it('should render the login page', function(done) { + this.res.render = page => { + page.should.equal('user/login') + return done() + } + return this.UserPagesController.loginPage(this.req, this.res) + }) + + return describe('when an explicit redirect is set via query string', function() { + beforeEach(function() { + this.AuthenticationController._getRedirectFromSession = sinon + .stub() + .returns(null) + this.AuthenticationController.setRedirectInSession = sinon.stub() + return (this.req.query.redir = '/somewhere/in/particular') + }) + + return it('should set a redirect', function(done) { + this.res.render = page => { + this.AuthenticationController.setRedirectInSession.callCount.should.equal( + 1 + ) + expect( + this.AuthenticationController.setRedirectInSession.lastCall.args[1] + ).to.equal(this.req.query.redir) + return done() + } + return this.UserPagesController.loginPage(this.req, this.res) + }) + }) + }) + + describe('sessionsPage', function() { + beforeEach(function() { + return this.UserSessionsManager.getAllUserSessions.callsArgWith( + 2, + null, + [] + ) + }) + + it('should render user/sessions', function(done) { + this.res.render = function(page) { + page.should.equal('user/sessions') + return done() + } + return this.UserPagesController.sessionsPage(this.req, this.res) + }) + + it('should have called getAllUserSessions', function(done) { + this.res.render = page => { + this.UserSessionsManager.getAllUserSessions.callCount.should.equal(1) + return done() + } + return this.UserPagesController.sessionsPage(this.req, this.res) + }) + + return describe('when getAllUserSessions produces an error', function() { + beforeEach(function() { + return this.UserSessionsManager.getAllUserSessions.callsArgWith( + 2, + new Error('woops') + ) + }) + + return it('should call next with an error', function(done) { + this.next = err => { + assert(err !== null) + assert(err instanceof Error) + return done() + } + return this.UserPagesController.sessionsPage( + this.req, + this.res, + this.next + ) + }) + }) + }) + + describe('settingsPage', function() { + beforeEach(function() { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, { has_password: true }) + return (this.UserGetter.getUser = sinon + .stub() + .callsArgWith(1, null, this.user)) + }) + + it('should render user/settings', function(done) { + this.res.render = function(page) { + page.should.equal('user/settings') + return done() + } + return this.UserPagesController.settingsPage(this.req, this.res) + }) + + it('should send user', function(done) { + this.res.render = (page, opts) => { + opts.user.should.equal(this.user) + return done() + } + return this.UserPagesController.settingsPage(this.req, this.res) + }) + + it("should set 'shouldAllowEditingDetails' to true", function(done) { + this.res.render = (page, opts) => { + opts.shouldAllowEditingDetails.should.equal(true) + return done() + } + return this.UserPagesController.settingsPage(this.req, this.res) + }) + + it('should restructure thirdPartyIdentifiers data for template use', function(done) { + const expectedResult = { + google: 'testId' + } + this.res.render = (page, opts) => { + expect(opts.thirdPartyIds).to.include(expectedResult) + return done() + } + return this.UserPagesController.settingsPage(this.req, this.res) + }) + + describe('when ldap.updateUserDetailsOnLogin is true', function() { + beforeEach(function() { + return (this.settings.ldap = { updateUserDetailsOnLogin: true }) + }) + + afterEach(function() { + return delete this.settings.ldap + }) + + return it('should set "shouldAllowEditingDetails" to false', function(done) { + this.res.render = (page, opts) => { + opts.shouldAllowEditingDetails.should.equal(false) + return done() + } + return this.UserPagesController.settingsPage(this.req, this.res) + }) + }) + + return describe('when saml.updateUserDetailsOnLogin is true', function() { + beforeEach(function() { + return (this.settings.saml = { updateUserDetailsOnLogin: true }) + }) + + afterEach(function() { + return delete this.settings.saml + }) + + return it('should set "shouldAllowEditingDetails" to false', function(done) { + this.res.render = (page, opts) => { + opts.shouldAllowEditingDetails.should.equal(false) + return done() + } + return this.UserPagesController.settingsPage(this.req, this.res) + }) + }) + }) + + return describe('activateAccountPage', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user) + this.req.query.user_id = this.user_id + return (this.req.query.token = this.token = 'mock-token-123') + }) + + it('should 404 without a user_id', function(done) { + delete this.req.query.user_id + this.ErrorController.notFound = () => done() + return this.UserPagesController.activateAccountPage(this.req, this.res) + }) + + it('should 404 without a token', function(done) { + delete this.req.query.token + this.ErrorController.notFound = () => done() + return this.UserPagesController.activateAccountPage(this.req, this.res) + }) + + it('should 404 without a valid user_id', function(done) { + this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, null) + this.ErrorController.notFound = () => done() + return this.UserPagesController.activateAccountPage(this.req, this.res) + }) + + it('should redirect activated users to login', function(done) { + this.user.loginCount = 1 + this.res.redirect = url => { + this.UserGetter.getUser.calledWith(this.user_id).should.equal(true) + url.should.equal(`/login?email=${encodeURIComponent(this.user.email)}`) + return done() + } + return this.UserPagesController.activateAccountPage(this.req, this.res) + }) + + return it('render the activation page if the user has not logged in before', function(done) { + this.user.loginCount = 0 + this.res.render = (page, opts) => { + page.should.equal('user/activate') + opts.email.should.equal(this.user.email) + opts.token.should.equal(this.token) + return done() + } + return this.UserPagesController.activateAccountPage(this.req, this.res) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserRegistrationHandlerTests.js b/services/web/test/unit/src/User/UserRegistrationHandlerTests.js new file mode 100644 index 0000000000..a737da3a56 --- /dev/null +++ b/services/web/test/unit/src/User/UserRegistrationHandlerTests.js @@ -0,0 +1,328 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/User/UserRegistrationHandler' +) +const sinon = require('sinon') +const { expect } = require('chai') +const EmailHelper = require('../../../../app/src/Features/Helpers/EmailHelper') + +describe('UserRegistrationHandler', function() { + beforeEach(function() { + this.user = { _id: (this.user_id = '31j2lk21kjl') } + this.User = { update: sinon.stub().callsArgWith(2) } + this.UserGetter = { getUserByAnyEmail: sinon.stub() } + this.UserCreator = { + createNewUser: sinon.stub().callsArgWith(1, null, this.user) + } + this.AuthenticationManager = { + validateEmail: sinon.stub().returns(null), + validatePassword: sinon.stub().returns(null), + setUserPassword: sinon.stub().callsArgWith(2) + } + this.NewsLetterManager = { subscribe: sinon.stub().callsArgWith(1) } + this.EmailHandler = { sendEmail: sinon.stub().callsArgWith(2) } + this.OneTimeTokenHandler = { getNewToken: sinon.stub() } + this.handler = SandboxedModule.require(modulePath, { + requires: { + '../../models/User': { User: this.User }, + './UserGetter': this.UserGetter, + './UserCreator': this.UserCreator, + '../Authentication/AuthenticationManager': this.AuthenticationManager, + '../Newsletter/NewsletterManager': this.NewsLetterManager, + 'logger-sharelatex': (this.logger = { log: sinon.stub() }), + crypto: (this.crypto = {}), + '../Email/EmailHandler': this.EmailHandler, + '../Security/OneTimeTokenHandler': this.OneTimeTokenHandler, + '../Analytics/AnalyticsManager': (this.AnalyticsManager = { + recordEvent: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + siteUrl: 'http://sl.example.com' + }), + '../Helpers/EmailHelper': EmailHelper + } + }) + + return (this.passingRequest = { + email: 'something@email.com', + password: '123' + }) + }) + + describe('validate Register Request', function() { + it('allows passing validation through', function() { + const result = this.handler._registrationRequestIsValid( + this.passingRequest + ) + return result.should.equal(true) + }) + + describe('failing email validation', function() { + beforeEach(function() { + return this.AuthenticationManager.validateEmail.returns({ + message: 'email not set' + }) + }) + + return it('does not allow through', function() { + const result = this.handler._registrationRequestIsValid( + this.passingRequest + ) + return result.should.equal(false) + }) + }) + + return describe('failing password validation', function() { + beforeEach(function() { + return this.AuthenticationManager.validatePassword.returns({ + message: 'password is too short' + }) + }) + + return it('does not allow through', function() { + const result = this.handler._registrationRequestIsValid( + this.passingRequest + ) + return result.should.equal(false) + }) + }) + }) + + describe('registerNewUser', function() { + describe('holdingAccount', function(done) { + beforeEach(function() { + this.user.holdingAccount = true + this.handler._registrationRequestIsValid = sinon.stub().returns(true) + return this.UserGetter.getUserByAnyEmail.callsArgWith( + 1, + null, + this.user + ) + }) + + it('should not create a new user if there is a holding account there', function(done) { + return this.handler.registerNewUser(this.passingRequest, err => { + this.UserCreator.createNewUser.called.should.equal(false) + return done() + }) + }) + + return it('should set holding account to false', function(done) { + return this.handler.registerNewUser(this.passingRequest, err => { + const update = this.User.update.args[0] + assert.deepEqual(update[0], { _id: this.user._id }) + assert.deepEqual(update[1], { $set: { holdingAccount: false } }) + return done() + }) + }) + }) + + describe('invalidRequest', function() { + it('should not create a new user if the the request is not valid', function(done) { + this.handler._registrationRequestIsValid = sinon.stub().returns(false) + return this.handler.registerNewUser(this.passingRequest, err => { + expect(err).to.exist + this.UserCreator.createNewUser.called.should.equal(false) + return done() + }) + }) + + return it('should return email registered in the error if there is a non holdingAccount there', function(done) { + this.UserGetter.getUserByAnyEmail.callsArgWith( + 1, + null, + (this.user = { holdingAccount: false }) + ) + return this.handler.registerNewUser( + this.passingRequest, + (err, user) => { + err.should.deep.equal(new Error('EmailAlreadyRegistered')) + user.should.deep.equal(this.user) + return done() + } + ) + }) + }) + + describe('validRequest', function() { + beforeEach(function() { + this.handler._registrationRequestIsValid = sinon.stub().returns(true) + return this.UserGetter.getUserByAnyEmail.callsArgWith(1) + }) + + it('should create a new user', function(done) { + return this.handler.registerNewUser(this.passingRequest, err => { + this.UserCreator.createNewUser + .calledWith({ + email: this.passingRequest.email, + holdingAccount: false, + first_name: this.passingRequest.first_name, + last_name: this.passingRequest.last_name + }) + .should.equal(true) + return done() + }) + }) + + it('lower case email', function(done) { + this.passingRequest.email = 'soMe@eMail.cOm' + return this.handler.registerNewUser(this.passingRequest, err => { + this.UserCreator.createNewUser.args[0][0].email.should.equal( + 'some@email.com' + ) + return done() + }) + }) + + it('trim white space from email', function(done) { + this.passingRequest.email = ' some@email.com ' + return this.handler.registerNewUser(this.passingRequest, err => { + this.UserCreator.createNewUser.args[0][0].email.should.equal( + 'some@email.com' + ) + return done() + }) + }) + + it('should set the password', function(done) { + return this.handler.registerNewUser(this.passingRequest, err => { + this.AuthenticationManager.setUserPassword + .calledWith(this.user._id, this.passingRequest.password) + .should.equal(true) + return done() + }) + }) + + it('should add the user to the newsletter if accepted terms', function(done) { + this.passingRequest.subscribeToNewsletter = 'true' + return this.handler.registerNewUser(this.passingRequest, err => { + this.NewsLetterManager.subscribe + .calledWith(this.user) + .should.equal(true) + return done() + }) + }) + + it('should not add the user to the newsletter if not accepted terms', function(done) { + return this.handler.registerNewUser(this.passingRequest, err => { + this.NewsLetterManager.subscribe + .calledWith(this.user) + .should.equal(false) + return done() + }) + }) + + return it('should track the registration event', function(done) { + return this.handler.registerNewUser(this.passingRequest, err => { + this.AnalyticsManager.recordEvent + .calledWith(this.user._id, 'user-registered') + .should.equal(true) + return done() + }) + }) + }) + + return it('should call the ReferalAllocator', done => done()) + }) + + return describe('registerNewUserAndSendActivationEmail', function() { + beforeEach(function() { + this.email = 'email@example.com' + this.crypto.randomBytes = sinon.stub().returns({ + toString: () => { + return (this.password = 'mock-password') + } + }) + this.OneTimeTokenHandler.getNewToken.yields( + null, + (this.token = 'mock-token') + ) + this.handler.registerNewUser = sinon.stub() + return (this.callback = sinon.stub()) + }) + + describe('with a new user', function() { + beforeEach(function() { + this.handler.registerNewUser.callsArgWith(1, null, this.user) + return this.handler.registerNewUserAndSendActivationEmail( + this.email, + this.callback + ) + }) + + it('should ask the UserRegistrationHandler to register user', function() { + return this.handler.registerNewUser + .calledWith({ + email: this.email, + password: this.password + }) + .should.equal(true) + }) + + it('should generate a new password reset token', function() { + return this.OneTimeTokenHandler.getNewToken + .calledWith('password', this.user_id, { expiresIn: 7 * 24 * 60 * 60 }) + .should.equal(true) + }) + + it('should send a registered email', function() { + return this.EmailHandler.sendEmail + .calledWith('registered', { + to: this.user.email, + setNewPasswordUrl: `${this.settings.siteUrl}/user/activate?token=${ + this.token + }&user_id=${this.user_id}` + }) + .should.equal(true) + }) + + return it('should return the user', function() { + return this.callback + .calledWith( + null, + this.user, + `${this.settings.siteUrl}/user/activate?token=${ + this.token + }&user_id=${this.user_id}` + ) + .should.equal(true) + }) + }) + + return describe('with a user that already exists', function() { + beforeEach(function() { + this.handler.registerNewUser.callsArgWith( + 1, + new Error('EmailAlreadyRegistered'), + this.user + ) + return this.handler.registerNewUserAndSendActivationEmail( + this.email, + this.callback + ) + }) + + return it('should still generate a new password token and email', function() { + this.OneTimeTokenHandler.getNewToken.called.should.equal(true) + return this.EmailHandler.sendEmail.called.should.equal(true) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserSessionsManagerTests.js b/services/web/test/unit/src/User/UserSessionsManagerTests.js new file mode 100644 index 0000000000..2d04acd1c6 --- /dev/null +++ b/services/web/test/unit/src/User/UserSessionsManagerTests.js @@ -0,0 +1,812 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/Features/User/UserSessionsManager.js' +const SandboxedModule = require('sandboxed-module') +const Async = require('async') + +describe('UserSessionsManager', function() { + beforeEach(function() { + this.user = { + _id: 'abcd', + email: 'user@example.com' + } + this.sessionId = 'some_session_id' + + this.rclient = { + multi: sinon.stub(), + exec: sinon.stub(), + get: sinon.stub(), + del: sinon.stub(), + sadd: sinon.stub(), + srem: sinon.stub(), + smembers: sinon.stub(), + mget: sinon.stub(), + expire: sinon.stub() + } + this.rclient.multi.returns(this.rclient) + this.rclient.get.returns(this.rclient) + this.rclient.del.returns(this.rclient) + this.rclient.sadd.returns(this.rclient) + this.rclient.srem.returns(this.rclient) + this.rclient.smembers.returns(this.rclient) + this.rclient.expire.returns(this.rclient) + this.rclient.exec.callsArgWith(0, null) + + this.UserSessionsRedis = { + client: () => this.rclient, + sessionSetKey: user => `UserSessions:{${user._id}}` + } + this.logger = { + err: sinon.stub(), + error: sinon.stub(), + log: sinon.stub() + } + this.settings = { + redis: { + web: {} + } + } + return (this.UserSessionsManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': this.logger, + 'settings-sharelatex': this.settings, + './UserSessionsRedis': this.UserSessionsRedis, + async: Async + } + })) + }) + + describe('_sessionKey', () => + it('should build the correct key', function() { + const result = this.UserSessionsManager._sessionKey(this.sessionId) + return result.should.equal('sess:some_session_id') + })) + + describe('trackSession', function() { + beforeEach(function() { + this.call = callback => { + return this.UserSessionsManager.trackSession( + this.user, + this.sessionId, + callback + ) + } + this.rclient.exec.callsArgWith(0, null) + return (this._checkSessions = sinon + .stub(this.UserSessionsManager, '_checkSessions') + .returns(null)) + }) + + afterEach(function() { + return this._checkSessions.restore() + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + return done() + }) + }) + + it('should call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.multi.callCount.should.equal(1) + this.rclient.sadd.callCount.should.equal(1) + this.rclient.expire.callCount.should.equal(1) + this.rclient.exec.callCount.should.equal(1) + return done() + }) + }) + + it('should call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(1) + return done() + }) + }) + + describe('when rclient produces an error', function() { + beforeEach(function() { + return this.rclient.exec.callsArgWith(0, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when no user is supplied', function() { + beforeEach(function() { + return (this.call = callback => { + return this.UserSessionsManager.trackSession( + null, + this.sessionId, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should not call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.multi.callCount.should.equal(0) + this.rclient.sadd.callCount.should.equal(0) + this.rclient.expire.callCount.should.equal(0) + this.rclient.exec.callCount.should.equal(0) + return done() + }) + }) + + return it('should not call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when no sessionId is supplied', function() { + beforeEach(function() { + return (this.call = callback => { + return this.UserSessionsManager.trackSession( + this.user, + null, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should not call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.multi.callCount.should.equal(0) + this.rclient.sadd.callCount.should.equal(0) + this.rclient.expire.callCount.should.equal(0) + this.rclient.exec.callCount.should.equal(0) + return done() + }) + }) + + return it('should not call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(0) + return done() + }) + }) + }) + }) + + describe('untrackSession', function() { + beforeEach(function() { + this.call = callback => { + return this.UserSessionsManager.untrackSession( + this.user, + this.sessionId, + callback + ) + } + this.rclient.exec.callsArgWith(0, null) + return (this._checkSessions = sinon + .stub(this.UserSessionsManager, '_checkSessions') + .returns(null)) + }) + + afterEach(function() { + return this._checkSessions.restore() + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(undefined) + return done() + }) + }) + + it('should call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.multi.callCount.should.equal(1) + this.rclient.srem.callCount.should.equal(1) + this.rclient.expire.callCount.should.equal(1) + this.rclient.exec.callCount.should.equal(1) + return done() + }) + }) + + it('should call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(1) + return done() + }) + }) + + describe('when rclient produces an error', function() { + beforeEach(function() { + return this.rclient.exec.callsArgWith(0, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when no user is supplied', function() { + beforeEach(function() { + return (this.call = callback => { + return this.UserSessionsManager.untrackSession( + null, + this.sessionId, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should not call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.multi.callCount.should.equal(0) + this.rclient.srem.callCount.should.equal(0) + this.rclient.expire.callCount.should.equal(0) + this.rclient.exec.callCount.should.equal(0) + return done() + }) + }) + + return it('should not call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when no sessionId is supplied', function() { + beforeEach(function() { + return (this.call = callback => { + return this.UserSessionsManager.untrackSession( + this.user, + null, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should not call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.multi.callCount.should.equal(0) + this.rclient.srem.callCount.should.equal(0) + this.rclient.expire.callCount.should.equal(0) + this.rclient.exec.callCount.should.equal(0) + return done() + }) + }) + + return it('should not call _checkSessions', function(done) { + return this.call(err => { + this._checkSessions.callCount.should.equal(0) + return done() + }) + }) + }) + }) + + describe('revokeAllUserSessions', function() { + beforeEach(function() { + this.sessionKeys = ['sess:one', 'sess:two'] + this.retain = [] + this.rclient.smembers.callsArgWith(1, null, this.sessionKeys) + this.rclient.del = sinon.stub().callsArgWith(1, null) + this.rclient.srem = sinon.stub().callsArgWith(2, null) + return (this.call = callback => { + return this.UserSessionsManager.revokeAllUserSessions( + this.user, + this.retain, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(1) + + this.rclient.del.callCount.should.equal(2) + expect(this.rclient.del.firstCall.args[0]).to.deep.equal( + this.sessionKeys[0] + ) + expect(this.rclient.del.secondCall.args[0]).to.deep.equal( + this.sessionKeys[1] + ) + + this.rclient.srem.callCount.should.equal(1) + expect(this.rclient.srem.firstCall.args[1]).to.deep.equal( + this.sessionKeys + ) + + return done() + }) + }) + + describe('when a session is retained', function() { + beforeEach(function() { + this.sessionKeys = ['sess:one', 'sess:two', 'sess:three', 'sess:four'] + this.retain = ['two'] + this.rclient.smembers.callsArgWith(1, null, this.sessionKeys) + this.rclient.del = sinon.stub().callsArgWith(1, null) + return (this.call = callback => { + return this.UserSessionsManager.revokeAllUserSessions( + this.user, + this.retain, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(1) + this.rclient.del.callCount.should.equal(this.sessionKeys.length - 1) + this.rclient.srem.callCount.should.equal(1) + return done() + }) + }) + + return it('should remove all sessions except for the retained one', function(done) { + return this.call(err => { + expect(this.rclient.del.firstCall.args[0]).to.deep.equal('sess:one') + expect(this.rclient.del.secondCall.args[0]).to.deep.equal( + 'sess:three' + ) + expect(this.rclient.del.thirdCall.args[0]).to.deep.equal('sess:four') + expect(this.rclient.srem.firstCall.args[1]).to.deep.equal([ + 'sess:one', + 'sess:three', + 'sess:four' + ]) + return done() + }) + }) + }) + + describe('when rclient produces an error', function() { + beforeEach(function() { + return (this.rclient.del = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not call rclient.srem', function(done) { + return this.call(err => { + this.rclient.srem.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when no user is supplied', function() { + beforeEach(function() { + return (this.call = callback => { + return this.UserSessionsManager.revokeAllUserSessions( + null, + this.retain, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + return it('should not call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(0) + this.rclient.del.callCount.should.equal(0) + this.rclient.srem.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when there are no keys to delete', function() { + beforeEach(function() { + return this.rclient.smembers.callsArgWith(1, null, []) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + return it('should not do the delete operation', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(1) + this.rclient.del.callCount.should.equal(0) + this.rclient.srem.callCount.should.equal(0) + return done() + }) + }) + }) + }) + + describe('touch', function() { + beforeEach(function() { + this.rclient.expire.callsArgWith(2, null) + return (this.call = callback => { + return this.UserSessionsManager.touch(this.user, callback) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + it('should call rclient.expire', function(done) { + return this.call(err => { + this.rclient.expire.callCount.should.equal(1) + return done() + }) + }) + + describe('when rclient produces an error', function() { + beforeEach(function() { + return this.rclient.expire.callsArgWith(2, new Error('woops')) + }) + + return it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + + return describe('when no user is supplied', function() { + beforeEach(function() { + return (this.call = callback => { + return this.UserSessionsManager.touch(null, callback) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + return it('should not call expire', function(done) { + return this.call(err => { + this.rclient.expire.callCount.should.equal(0) + return done() + }) + }) + }) + }) + + describe('getAllUserSessions', function() { + beforeEach(function() { + this.sessionKeys = ['sess:one', 'sess:two', 'sess:three'] + this.sessions = [ + '{"user": {"ip_address": "a", "session_created": "b"}}', + '{"passport": {"user": {"ip_address": "c", "session_created": "d"}}}' + ] + this.exclude = ['two'] + this.rclient.smembers.callsArgWith(1, null, this.sessionKeys) + this.rclient.get = sinon.stub() + this.rclient.get.onCall(0).callsArgWith(1, null, this.sessions[0]) + this.rclient.get.onCall(1).callsArgWith(1, null, this.sessions[1]) + + return (this.call = callback => { + return this.UserSessionsManager.getAllUserSessions( + this.user, + this.exclude, + callback + ) + }) + }) + + it('should not produce an error', function(done) { + return this.call((err, sessions) => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should get sessions', function(done) { + return this.call((err, sessions) => { + expect(sessions).to.deep.equal([ + { ip_address: 'a', session_created: 'b' }, + { ip_address: 'c', session_created: 'd' } + ]) + return done() + }) + }) + + it('should have called rclient.smembers', function(done) { + return this.call((err, sessions) => { + this.rclient.smembers.callCount.should.equal(1) + return done() + }) + }) + + it('should have called rclient.get', function(done) { + return this.call((err, sessions) => { + this.rclient.get.callCount.should.equal(this.sessionKeys.length - 1) + return done() + }) + }) + + describe('when there are no other sessions', function() { + beforeEach(function() { + this.sessionKeys = ['sess:two'] + return this.rclient.smembers.callsArgWith(1, null, this.sessionKeys) + }) + + it('should not produce an error', function(done) { + return this.call((err, sessions) => { + expect(err).to.equal(null) + return done() + }) + }) + + it('should produce an empty list of sessions', function(done) { + return this.call((err, sessions) => { + expect(sessions).to.deep.equal([]) + return done() + }) + }) + + it('should have called rclient.smembers', function(done) { + return this.call((err, sessions) => { + this.rclient.smembers.callCount.should.equal(1) + return done() + }) + }) + + return it('should not have called rclient.mget', function(done) { + return this.call((err, sessions) => { + this.rclient.mget.callCount.should.equal(0) + return done() + }) + }) + }) + + describe('when smembers produces an error', function() { + beforeEach(function() { + return this.rclient.smembers.callsArgWith(1, new Error('woops')) + }) + + it('should produce an error', function(done) { + return this.call((err, sessions) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should not have called rclient.mget', function(done) { + return this.call((err, sessions) => { + this.rclient.mget.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when get produces an error', function() { + beforeEach(function() { + return (this.rclient.get = sinon + .stub() + .callsArgWith(1, new Error('woops'))) + }) + + return it('should produce an error', function(done) { + return this.call((err, sessions) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) + }) + + return describe('_checkSessions', function() { + beforeEach(function() { + this.call = callback => { + return this.UserSessionsManager._checkSessions(this.user, callback) + } + this.sessionKeys = ['one', 'two'] + this.rclient.smembers.callsArgWith(1, null, this.sessionKeys) + this.rclient.get.callsArgWith(1, null, 'some-value') + return this.rclient.srem.callsArgWith(2, null, {}) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(undefined) + return done() + }) + }) + + it('should call the appropriate redis methods', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(1) + this.rclient.get.callCount.should.equal(2) + this.rclient.srem.callCount.should.equal(0) + return done() + }) + }) + + describe('when one of the keys is not present in redis', function() { + beforeEach(function() { + this.rclient.get.onCall(0).callsArgWith(1, null, 'some-val') + return this.rclient.get.onCall(1).callsArgWith(1, null, null) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(undefined) + return done() + }) + }) + + return it('should remove that key from the set', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(1) + this.rclient.get.callCount.should.equal(2) + this.rclient.srem.callCount.should.equal(1) + this.rclient.srem.firstCall.args[1].should.equal('two') + return done() + }) + }) + }) + + describe('when no user is supplied', function() { + beforeEach(function() { + return (this.call = callback => { + return this.UserSessionsManager._checkSessions(null, callback) + }) + }) + + it('should not produce an error', function(done) { + return this.call(err => { + expect(err).to.not.be.instanceof(Error) + expect(err).to.equal(null) + return done() + }) + }) + + return it('should not call redis methods', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(0) + this.rclient.get.callCount.should.equal(0) + return done() + }) + }) + }) + + return describe('when one of the get operations produces an error', function() { + beforeEach(function() { + this.rclient.get.onCall(0).callsArgWith(1, new Error('woops'), null) + return this.rclient.get.onCall(1).callsArgWith(1, null, null) + }) + + it('should produce an error', function(done) { + return this.call(err => { + expect(err).to.be.instanceof(Error) + return done() + }) + }) + + return it('should call the right redis methods, bailing out early', function(done) { + return this.call(err => { + this.rclient.smembers.callCount.should.equal(1) + this.rclient.get.callCount.should.equal(1) + this.rclient.srem.callCount.should.equal(0) + return done() + }) + }) + }) + }) +}) diff --git a/services/web/test/unit/src/User/UserUpdaterTests.js b/services/web/test/unit/src/User/UserUpdaterTests.js new file mode 100644 index 0000000000..501eb59ecc --- /dev/null +++ b/services/web/test/unit/src/User/UserUpdaterTests.js @@ -0,0 +1,510 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/Features/User/UserUpdater' +) +const { expect } = require('chai') +const tk = require('timekeeper') + +describe('UserUpdater', function() { + beforeEach(function() { + tk.freeze(Date.now()) + this.mongojs = { + db: {}, + ObjectId(id) { + return id + } + } + this.UserGetter = { + getUserEmail: sinon.stub(), + getUserByAnyEmail: sinon.stub(), + ensureUniqueEmailAddress: sinon.stub() + } + this.logger = { + err: sinon.stub(), + log() {}, + warn() {} + } + this.addAffiliation = sinon.stub().yields() + this.removeAffiliation = sinon.stub().callsArgWith(2, null) + this.refreshFeatures = sinon.stub().yields() + this.NewsletterManager = { changeEmail: sinon.stub() } + this.UserUpdater = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': this.logger, + '../../infrastructure/mongojs': this.mongojs, + 'metrics-sharelatex': { + timeAsyncMethod: sinon.stub() + }, + './UserGetter': this.UserGetter, + '../Institutions/InstitutionsAPI': { + addAffiliation: this.addAffiliation, + removeAffiliation: this.removeAffiliation + }, + '../Subscription/FeaturesUpdater': { + refreshFeatures: this.refreshFeatures + }, + 'settings-sharelatex': (this.settings = {}), + request: (this.request = {}), + '../Newsletter/NewsletterManager': this.NewsletterManager + } + }) + + this.stubbedUser = { + _id: '3131231', + name: 'bob', + email: 'hello@world.com' + } + return (this.newEmail = 'bob@bob.com') + }) + + afterEach(() => tk.reset()) + + describe('changeEmailAddress', function() { + beforeEach(function() { + this.UserGetter.getUserEmail.callsArgWith(1, null, this.stubbedUser.email) + this.UserUpdater.addEmailAddress = sinon.stub().callsArgWith(2) + this.UserUpdater.setDefaultEmailAddress = sinon.stub().callsArgWith(2) + return (this.UserUpdater.removeEmailAddress = sinon + .stub() + .callsArgWith(2)) + }) + + it('change email', function(done) { + return this.UserUpdater.changeEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.UserUpdater.addEmailAddress + .calledWith(this.stubbedUser._id, this.newEmail) + .should.equal(true) + this.UserUpdater.setDefaultEmailAddress + .calledWith(this.stubbedUser._id, this.newEmail) + .should.equal(true) + this.UserUpdater.removeEmailAddress + .calledWith(this.stubbedUser._id, this.stubbedUser.email) + .should.equal(true) + return done() + } + ) + }) + + it('validates email', function(done) { + return this.UserUpdater.changeEmailAddress( + this.stubbedUser._id, + 'foo', + err => { + should.exist(err) + return done() + } + ) + }) + + return it('handle error', function(done) { + this.UserUpdater.removeEmailAddress.callsArgWith(2, new Error('nope')) + return this.UserUpdater.changeEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + return done() + } + ) + }) + }) + + describe('addEmailAddress', function() { + beforeEach(function() { + this.UserGetter.ensureUniqueEmailAddress = sinon.stub().callsArgWith(1) + return (this.UserUpdater.updateUser = sinon.stub().callsArgWith(2, null)) + }) + + it('add email', function(done) { + return this.UserUpdater.addEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + this.UserGetter.ensureUniqueEmailAddress.called.should.equal(true) + should.not.exist(err) + const reversedHostname = this.newEmail + .split('@')[1] + .split('') + .reverse() + .join('') + this.UserUpdater.updateUser + .calledWith(this.stubbedUser._id, { + $push: { + emails: { + email: this.newEmail, + createdAt: sinon.match.date, + reversedHostname + } + } + }) + .should.equal(true) + return done() + } + ) + }) + + it('add affiliation', function(done) { + const affiliationOptions = { + university: { id: 1 }, + role: 'Prof', + department: 'Math' + } + return this.UserUpdater.addEmailAddress( + this.stubbedUser._id, + this.newEmail, + affiliationOptions, + err => { + should.not.exist(err) + this.addAffiliation.calledOnce.should.equal(true) + const { args } = this.addAffiliation.lastCall + args[0].should.equal(this.stubbedUser._id) + args[1].should.equal(this.newEmail) + args[2].should.equal(affiliationOptions) + return done() + } + ) + }) + + it('handle error', function(done) { + this.UserUpdater.updateUser = sinon + .stub() + .callsArgWith(2, new Error('nope')) + + return this.UserUpdater.addEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + this.logger.err.called.should.equal(true) + should.exist(err) + return done() + } + ) + }) + + it('handle affiliation error', function(done) { + const body = { errors: 'affiliation error message' } + this.addAffiliation.callsArgWith(3, new Error('nope')) + return this.UserUpdater.addEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + this.UserUpdater.updateUser.called.should.equal(false) + return done() + } + ) + }) + + return it('validates email', function(done) { + return this.UserUpdater.addEmailAddress( + this.stubbedUser._id, + 'bar', + err => { + should.exist(err) + return done() + } + ) + }) + }) + + describe('removeEmailAddress', function() { + beforeEach(function() { + return (this.UserUpdater.updateUser = sinon + .stub() + .callsArgWith(2, null, { nMatched: 1 })) + }) + + it('remove email', function(done) { + return this.UserUpdater.removeEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.UserUpdater.updateUser + .calledWith( + { _id: this.stubbedUser._id, email: { $ne: this.newEmail } }, + { $pull: { emails: { email: this.newEmail } } } + ) + .should.equal(true) + return done() + } + ) + }) + + it('remove affiliation', function(done) { + return this.UserUpdater.removeEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.removeAffiliation.calledOnce.should.equal(true) + const { args } = this.removeAffiliation.lastCall + args[0].should.equal(this.stubbedUser._id) + args[1].should.equal(this.newEmail) + return done() + } + ) + }) + + it('handle error', function(done) { + this.UserUpdater.updateUser = sinon + .stub() + .callsArgWith(2, new Error('nope')) + + return this.UserUpdater.removeEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + return done() + } + ) + }) + + it('handle missed update', function(done) { + this.UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, { n: 0 }) + + return this.UserUpdater.removeEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + return done() + } + ) + }) + + it('handle affiliation error', function(done) { + this.removeAffiliation.callsArgWith(2, new Error('nope')) + return this.UserUpdater.removeEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + this.UserUpdater.updateUser.called.should.equal(false) + return done() + } + ) + }) + + return it('validates email', function(done) { + return this.UserUpdater.removeEmailAddress( + this.stubbedUser._id, + 'baz', + err => { + should.exist(err) + return done() + } + ) + }) + }) + + describe('setDefaultEmailAddress', function() { + beforeEach(function() { + this.UserGetter.getUserEmail.callsArgWith(1, null, this.stubbedUser.email) + return this.NewsletterManager.changeEmail.callsArgWith(2, null) + }) + + it('set default', function(done) { + this.UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, { n: 1 }) + + return this.UserUpdater.setDefaultEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.UserUpdater.updateUser + .calledWith( + { _id: this.stubbedUser._id, 'emails.email': this.newEmail }, + { $set: { email: this.newEmail } } + ) + .should.equal(true) + return done() + } + ) + }) + + it('set changed the email in newsletter', function(done) { + this.UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, { n: 1 }) + + return this.UserUpdater.setDefaultEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.NewsletterManager.changeEmail + .calledWith(this.stubbedUser.email, this.newEmail) + .should.equal(true) + return done() + } + ) + }) + + it('handle error', function(done) { + this.UserUpdater.updateUser = sinon + .stub() + .callsArgWith(2, new Error('nope')) + + return this.UserUpdater.setDefaultEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + return done() + } + ) + }) + + it('handle missed update', function(done) { + this.UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, { n: 0 }) + + return this.UserUpdater.setDefaultEmailAddress( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + return done() + } + ) + }) + + return it('validates email', function(done) { + return this.UserUpdater.setDefaultEmailAddress( + this.stubbedUser._id, + '.edu', + err => { + should.exist(err) + return done() + } + ) + }) + }) + + return describe('confirmEmail', function() { + beforeEach(function() { + return (this.UserUpdater.updateUser = sinon + .stub() + .callsArgWith(2, null, { n: 1 })) + }) + + it('should update the email record', function(done) { + return this.UserUpdater.confirmEmail( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.UserUpdater.updateUser + .calledWith( + { _id: this.stubbedUser._id, 'emails.email': this.newEmail }, + { $set: { 'emails.$.confirmedAt': new Date() } } + ) + .should.equal(true) + return done() + } + ) + }) + + it('add affiliation', function(done) { + return this.UserUpdater.confirmEmail( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + this.addAffiliation.calledOnce.should.equal(true) + sinon.assert.calledWith( + this.addAffiliation, + this.stubbedUser._id, + this.newEmail, + { confirmedAt: new Date() } + ) + return done() + } + ) + }) + + it('handle error', function(done) { + this.UserUpdater.updateUser = sinon + .stub() + .callsArgWith(2, new Error('nope')) + + return this.UserUpdater.confirmEmail( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + return done() + } + ) + }) + + it('handle missed update', function(done) { + this.UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, { n: 0 }) + + return this.UserUpdater.confirmEmail( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + return done() + } + ) + }) + + it('validates email', function(done) { + return this.UserUpdater.confirmEmail(this.stubbedUser._id, '@', err => { + should.exist(err) + return done() + }) + }) + + it('handle affiliation error', function(done) { + this.addAffiliation.callsArgWith(3, new Error('nope')) + return this.UserUpdater.confirmEmail( + this.stubbedUser._id, + this.newEmail, + err => { + should.exist(err) + this.UserUpdater.updateUser.called.should.equal(false) + return done() + } + ) + }) + + return it('refresh features', function(done) { + return this.UserUpdater.confirmEmail( + this.stubbedUser._id, + this.newEmail, + err => { + should.not.exist(err) + sinon.assert.calledWith( + this.refreshFeatures, + this.stubbedUser._id, + true + ) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipAuthorizationTests.js b/services/web/test/unit/src/UserMembership/UserMembershipAuthorizationTests.js new file mode 100644 index 0000000000..76f981f676 --- /dev/null +++ b/services/web/test/unit/src/UserMembership/UserMembershipAuthorizationTests.js @@ -0,0 +1,321 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const { expect } = require('chai') +const modulePath = + '../../../../app/src/Features/UserMembership/UserMembershipAuthorization.js' +const SandboxedModule = require('sandboxed-module') +const MockRequest = require('../helpers/MockRequest') +const EntityConfigs = require('../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('UserMembershipAuthorization', function() { + beforeEach(function() { + this.req = new MockRequest() + this.req.params.id = 'mock-entity-id' + this.user = { _id: 'mock-user-id' } + this.subscription = { _id: 'mock-subscription-id' } + + this.AuthenticationController = { + getSessionUser: sinon.stub().returns(this.user) + } + this.UserMembershipHandler = { + getEntity: sinon.stub().yields(null, this.subscription), + getEntityWithoutAuthorizationCheck: sinon + .stub() + .yields(null, this.subscription) + } + this.AuthorizationMiddleware = { + redirectToRestricted: sinon.stub().yields(), + ensureUserIsSiteAdmin: sinon.stub().yields() + } + return (this.UserMembershipAuthorization = SandboxedModule.require( + modulePath, + { + requires: { + '../Authentication/AuthenticationController': this + .AuthenticationController, + '../Authorization/AuthorizationMiddleware': this + .AuthorizationMiddleware, + './UserMembershipHandler': this.UserMembershipHandler, + './EntityConfigs': EntityConfigs, + '../Errors/Errors': Errors, + request: (this.request = sinon.stub().yields(null, null, {})), + 'logger-sharelatex': { + log() {}, + err() {} + } + } + } + )) + }) + + describe('requireAccessToEntity', function() { + it('get entity', function(done) { + return this.UserMembershipAuthorization.requireGroupMetricsAccess( + this.req, + null, + error => { + expect(error).to.not.extist + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getEntity, + this.req.params.id, + { modelName: 'Subscription' }, + this.user + ) + expect(this.req.entity).to.equal(this.subscription) + expect(this.req.entityConfig).to.exist + return done() + } + ) + }) + + it('handle entity not found as non-admin', function(done) { + this.UserMembershipHandler.getEntity.yields(null, null) + this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields( + null, + null + ) + return this.UserMembershipAuthorization.requireGroupMetricsAccess( + this.req, + null, + error => { + expect(error).to.extist + expect(error).to.be.instanceof(Error) + expect(error.constructor.name).to.equal('NotFoundError') + sinon.assert.called(this.UserMembershipHandler.getEntity) + expect(this.req.entity).to.not.exist + return done() + } + ) + }) + + it('handle entity not found an admin can create', function(done) { + this.user.isAdmin = true + this.UserMembershipHandler.getEntity.yields(null, null) + this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields( + null, + null + ) + return this.UserMembershipAuthorization.requirePublisherMetricsAccess( + this.req, + { + redirect: path => { + expect(path).to.extist + expect(path).to.match(/create/) + return done() + } + } + ) + }) + + it('handle entity not found a non-admin can create', function(done) { + this.user.staffAccess = { institutionManagement: true } + this.UserMembershipHandler.getEntity.yields(null, null) + this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields( + null, + null + ) + return this.UserMembershipAuthorization.requirePublisherMetricsAccess( + this.req, + { + redirect: path => { + expect(path).to.extist + expect(path).to.match(/create/) + return done() + } + } + ) + }) + + it('handle entity not found an admin cannot create', function(done) { + this.user.isAdmin = true + this.UserMembershipHandler.getEntity.yields(null, null) + this.UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields( + null, + null + ) + return this.UserMembershipAuthorization.requireGroupMetricsAccess( + this.req, + null, + error => { + expect(error).to.extist + expect(error).to.be.instanceof(Error) + expect(error.constructor.name).to.equal('NotFoundError') + return done() + } + ) + }) + + it('handle entity no access', function(done) { + this.UserMembershipHandler.getEntity.yields(null, null) + return this.UserMembershipAuthorization.requireGroupMetricsAccess( + this.req, + null, + error => { + sinon.assert.called(this.AuthorizationMiddleware.redirectToRestricted) + return done() + } + ) + }) + + return it('handle anonymous user', function(done) { + this.AuthenticationController.getSessionUser.returns(null) + return this.UserMembershipAuthorization.requireGroupMetricsAccess( + this.req, + null, + error => { + expect(error).to.extist + sinon.assert.called(this.AuthorizationMiddleware.redirectToRestricted) + sinon.assert.notCalled(this.UserMembershipHandler.getEntity) + expect(this.req.entity).to.not.exist + return done() + } + ) + }) + }) + + return describe('requireEntityAccess', function() { + it('handle team access', function(done) { + return this.UserMembershipAuthorization.requireTeamMetricsAccess( + this.req, + null, + error => { + expect(error).to.not.extist + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getEntity, + this.req.params.id, + { fields: { primaryKey: 'overleaf.id' } } + ) + return done() + } + ) + }) + + it('handle group access', function(done) { + return this.UserMembershipAuthorization.requireGroupMetricsAccess( + this.req, + null, + error => { + expect(error).to.not.extist + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getEntity, + this.req.params.id, + { translations: { title: 'group_account' } } + ) + return done() + } + ) + }) + + it('handle group managers access', function(done) { + return this.UserMembershipAuthorization.requireGroupManagersManagementAccess( + this.req, + null, + error => { + expect(error).to.not.extist + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getEntity, + this.req.params.id, + { translations: { subtitle: 'managers_management' } } + ) + return done() + } + ) + }) + + it('handle institution access', function(done) { + return this.UserMembershipAuthorization.requireInstitutionMetricsAccess( + this.req, + null, + error => { + expect(error).to.not.extist + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getEntity, + this.req.params.id, + { modelName: 'Institution' } + ) + return done() + } + ) + }) + + it('handle template with brand access', function(done) { + const templateData = { + id: 123, + title: 'Template Title', + brand: { slug: 'brand-slug' } + } + this.request.yields( + null, + { statusCode: 200 }, + JSON.stringify(templateData) + ) + return this.UserMembershipAuthorization.requireTemplateMetricsAccess( + this.req, + null, + error => { + expect(error).to.not.extist + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getEntity, + 'brand-slug', + { modelName: 'Publisher' } + ) + return done() + } + ) + }) + + it('handle template without brand access', function(done) { + const templateData = { + id: 123, + title: 'Template Title', + brand: null + } + this.request.yields( + null, + { statusCode: 200 }, + JSON.stringify(templateData) + ) + return this.UserMembershipAuthorization.requireTemplateMetricsAccess( + this.req, + null, + error => { + expect(error).to.not.extist + sinon.assert.notCalled(this.UserMembershipHandler.getEntity) + sinon.assert.calledOnce( + this.AuthorizationMiddleware.ensureUserIsSiteAdmin + ) + return done() + } + ) + }) + + return it('handle graph access', function(done) { + this.req.query.resource_id = 'mock-resource-id' + this.req.query.resource_type = 'institution' + const middleware = this.UserMembershipAuthorization.requireGraphAccess + return middleware(this.req, null, error => { + expect(error).to.not.extist + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getEntity, + this.req.query.resource_id, + { modelName: 'Institution' } + ) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.js b/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.js new file mode 100644 index 0000000000..df91f0db2e --- /dev/null +++ b/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.js @@ -0,0 +1,363 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const assertCalledWith = sinon.assert.calledWith +const assertNotCalled = sinon.assert.notCalled +const chai = require('chai') +const should = chai.should() +const { assert } = chai +const { expect } = require('chai') +const modulePath = + '../../../../app/src/Features/UserMembership/UserMembershipController.js' +const SandboxedModule = require('sandboxed-module') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') +const EntityConfigs = require('../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs') +const Errors = require('../../../../app/src/Features/Errors/Errors') + +describe('UserMembershipController', function() { + beforeEach(function() { + this.req = new MockRequest() + this.req.params.id = 'mock-entity-id' + this.user = { _id: 'mock-user-id' } + this.newUser = { _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' } + this.subscription = { + _id: 'mock-subscription-id', + fetchV1Data: callback => callback(null, this.subscription) + } + this.institution = { + _id: 'mock-institution-id', + v1Id: 123, + fetchV1Data: callback => { + const institution = Object.assign({}, this.institution) + institution.name = 'Test Institution Name' + return callback(null, institution) + } + } + this.users = [ + { _id: 'mock-member-id-1', email: 'mock-email-1@foo.com' }, + { _id: 'mock-member-id-2', email: 'mock-email-2@foo.com' } + ] + + this.AuthenticationController = { + getSessionUser: sinon.stub().returns(this.user), + getLoggedInUserId: sinon.stub().returns(this.user._id) + } + this.UserMembershipHandler = { + getEntity: sinon.stub().yields(null, this.subscription), + createEntity: sinon.stub().yields(null, this.institution), + getUsers: sinon.stub().yields(null, this.users), + addUser: sinon.stub().yields(null, this.newUser), + removeUser: sinon.stub().yields(null) + } + return (this.UserMembershipController = SandboxedModule.require( + modulePath, + { + requires: { + '../Authentication/AuthenticationController': this + .AuthenticationController, + './UserMembershipHandler': this.UserMembershipHandler, + '../Errors/Errors': Errors, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + } + )) + }) + + describe('index', function() { + beforeEach(function() { + this.req.entity = this.subscription + return (this.req.entityConfig = EntityConfigs.group) + }) + + it('get users', function(done) { + return this.UserMembershipController.index(this.req, { + render: () => { + sinon.assert.calledWithMatch( + this.UserMembershipHandler.getUsers, + this.subscription, + { modelName: 'Subscription' } + ) + return done() + } + }) + }) + + it('render group view', function(done) { + return this.UserMembershipController.index(this.req, { + render: (viewPath, viewParams) => { + expect(viewPath).to.equal('user_membership/index') + expect(viewParams.users).to.deep.equal(this.users) + expect(viewParams.groupSize).to.equal(this.subscription.membersLimit) + expect(viewParams.translations.title).to.equal('group_account') + expect(viewParams.paths.addMember).to.equal( + `/manage/groups/${this.subscription._id}/invites` + ) + return done() + } + }) + }) + + it('render group managers view', function(done) { + this.req.entityConfig = EntityConfigs.groupManagers + return this.UserMembershipController.index(this.req, { + render: (viewPath, viewParams) => { + expect(viewPath).to.equal('user_membership/index') + expect(viewParams.groupSize).to.equal(undefined) + expect(viewParams.translations.title).to.equal('group_account') + expect(viewParams.translations.subtitle).to.equal( + 'managers_management' + ) + expect(viewParams.paths.exportMembers).to.be.undefined + return done() + } + }) + }) + + return it('render institution view', function(done) { + this.req.entity = this.institution + this.req.entityConfig = EntityConfigs.institution + return this.UserMembershipController.index(this.req, { + render: (viewPath, viewParams) => { + expect(viewPath).to.equal('user_membership/index') + expect(viewParams.name).to.equal('Test Institution Name') + expect(viewParams.groupSize).to.equal(undefined) + expect(viewParams.translations.title).to.equal('institution_account') + expect(viewParams.paths.exportMembers).to.be.undefined + return done() + } + }) + }) + }) + + describe('add', function() { + beforeEach(function() { + this.req.body.email = this.newUser.email + this.req.entity = this.subscription + return (this.req.entityConfig = EntityConfigs.groupManagers) + }) + + it('add user', function(done) { + return this.UserMembershipController.add(this.req, { + json: () => { + sinon.assert.calledWithMatch( + this.UserMembershipHandler.addUser, + this.subscription, + { modelName: 'Subscription' }, + this.newUser.email + ) + return done() + } + }) + }) + + it('return user object', function(done) { + return this.UserMembershipController.add(this.req, { + json: payload => { + payload.user.should.equal(this.newUser) + return done() + } + }) + }) + + it('handle readOnly entity', function(done) { + this.req.entityConfig = EntityConfigs.group + return this.UserMembershipController.add(this.req, null, error => { + expect(error).to.extist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + return done() + }) + }) + + it('handle user already added', function(done) { + this.UserMembershipHandler.addUser.yields({ alreadyAdded: true }) + return this.UserMembershipController.add(this.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('user_already_added') + return done() + } + }) + }) + }) + + it('handle user not found', function(done) { + this.UserMembershipHandler.addUser.yields({ userNotFound: true }) + return this.UserMembershipController.add(this.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('user_not_found') + return done() + } + }) + }) + }) + + return it('handle invalid email', function(done) { + this.req.body.email = 'not_valid_email' + return this.UserMembershipController.add(this.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('invalid_email') + return done() + } + }) + }) + }) + }) + + describe('remove', function() { + beforeEach(function() { + this.req.params.userId = this.newUser._id + this.req.entity = this.subscription + return (this.req.entityConfig = EntityConfigs.groupManagers) + }) + + it('remove user', function(done) { + return this.UserMembershipController.remove(this.req, { + send: () => { + sinon.assert.calledWithMatch( + this.UserMembershipHandler.removeUser, + this.subscription, + { modelName: 'Subscription' }, + this.newUser._id + ) + return done() + } + }) + }) + + it('handle readOnly entity', function(done) { + this.req.entityConfig = EntityConfigs.group + return this.UserMembershipController.remove(this.req, null, error => { + expect(error).to.extist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + return done() + }) + }) + + it('prevent self removal', function(done) { + this.req.params.userId = this.user._id + return this.UserMembershipController.remove(this.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('managers_cannot_remove_self') + return done() + } + }) + }) + }) + + return it('prevent admin removal', function(done) { + this.UserMembershipHandler.removeUser.yields({ isAdmin: true }) + return this.UserMembershipController.remove(this.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('managers_cannot_remove_admin') + return done() + } + }) + }) + }) + }) + + describe('exportCsv', function() { + beforeEach(function() { + this.req.entity = this.subscription + this.req.entityConfig = EntityConfigs.groupManagers + this.res = new MockResponse() + this.res.contentType = sinon.stub() + this.res.header = sinon.stub() + this.res.send = sinon.stub() + return this.UserMembershipController.exportCsv(this.req, this.res) + }) + + it('get users', function() { + return sinon.assert.calledWithMatch( + this.UserMembershipHandler.getUsers, + this.subscription, + { modelName: 'Subscription' } + ) + }) + + it('should set the correct content type on the request', function() { + return assertCalledWith(this.res.contentType, 'text/csv') + }) + + it('should name the exported csv file', function() { + return assertCalledWith( + this.res.header, + 'Content-Disposition', + 'attachment; filename=Group.csv' + ) + }) + + return it('should export the correct csv', function() { + return assertCalledWith( + this.res.send, + 'mock-email-1@foo.com\nmock-email-2@foo.com\n' + ) + }) + }) + + describe('new', function() { + beforeEach(function() { + this.req.params.name = 'publisher' + return (this.req.params.id = 'abc') + }) + + return it('renders view', function(done) { + return this.UserMembershipController.new(this.req, { + render: (viewPath, data) => { + expect(data.entityName).to.eq('publisher') + expect(data.entityId).to.eq('abc') + return done() + } + }) + }) + }) + + return describe('create', function() { + beforeEach(function() { + this.req.params.name = 'institution' + return (this.req.params.id = 123) + }) + + it('creates institution', function(done) { + return this.UserMembershipController.create(this.req, { + redirect: path => { + expect(path).to.eq(EntityConfigs['institution'].pathsFor(123).index) + sinon.assert.calledWithMatch( + this.UserMembershipHandler.createEntity, + 123, + { modelName: 'Institution' } + ) + return done() + } + }) + }) + + return it('checks canCreate', function(done) { + this.req.params.name = 'group' + return this.UserMembershipController.create(this.req, null, error => { + expect(error).to.extist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + sinon.assert.notCalled(this.UserMembershipHandler.createEntity) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipHandlerTests.js b/services/web/test/unit/src/UserMembership/UserMembershipHandlerTests.js new file mode 100644 index 0000000000..34cf5f9e4e --- /dev/null +++ b/services/web/test/unit/src/UserMembership/UserMembershipHandlerTests.js @@ -0,0 +1,406 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const should = chai.should() +const { expect } = require('chai') +const sinon = require('sinon') +const assertCalledWith = sinon.assert.calledWith +const assertNotCalled = sinon.assert.notCalled +const { ObjectId } = require('../../../../app/src/infrastructure/mongojs') +const modulePath = + '../../../../app/src/Features/UserMembership/UserMembershipHandler' +const SandboxedModule = require('sandboxed-module') +const Errors = require('../../../../app/src/Features/Errors/Errors') +const EntityConfigs = require('../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs') + +describe('UserMembershipHandler', function() { + beforeEach(function() { + this.user = { _id: ObjectId() } + this.newUser = { _id: ObjectId(), email: 'new-user-email@foo.bar' } + this.fakeEntityId = ObjectId() + this.subscription = { + _id: 'mock-subscription-id', + groupPlan: true, + membersLimit: 10, + member_ids: [ObjectId(), ObjectId()], + manager_ids: [ObjectId()], + invited_emails: ['mock-email-1@foo.com'], + teamInvites: [{ email: 'mock-email-1@bar.com' }], + update: sinon.stub().yields(null) + } + this.institution = { + _id: 'mock-institution-id', + v1Id: 123, + managerIds: [ObjectId(), ObjectId(), ObjectId()], + update: sinon.stub().yields(null) + } + this.publisher = { + _id: 'mock-publisher-id', + slug: 'slug', + managerIds: [ObjectId(), ObjectId()], + update: sinon.stub().yields(null) + } + + this.UserMembershipViewModel = { + buildAsync: sinon.stub().yields(null, { _id: 'mock-member-id' }), + build: sinon.stub().returns(this.newUser) + } + this.UserGetter = { + getUserByAnyEmail: sinon.stub().yields(null, this.newUser) + } + this.Institution = { findOne: sinon.stub().yields(null, this.institution) } + this.Subscription = { + findOne: sinon.stub().yields(null, this.subscription) + } + this.Publisher = { + findOne: sinon.stub().yields(null, this.publisher), + create: sinon.stub().yields(null, this.publisher) + } + return (this.UserMembershipHandler = SandboxedModule.require(modulePath, { + requires: { + './UserMembershipViewModel': this.UserMembershipViewModel, + '../User/UserGetter': this.UserGetter, + '../Errors/Errors': Errors, + '../../models/Institution': { + Institution: this.Institution + }, + '../../models/Subscription': { + Subscription: this.Subscription + }, + '../../models/Publisher': { + Publisher: this.Publisher + }, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + })) + }) + + describe('getEntity', () => + describe('group subscriptions', function() { + it('get subscription', function(done) { + return this.UserMembershipHandler.getEntity( + this.fakeEntityId, + EntityConfigs.group, + this.user, + null, + (error, subscription) => { + should.not.exist(error) + const expectedQuery = { + groupPlan: true, + _id: this.fakeEntityId, + manager_ids: ObjectId(this.user._id) + } + assertCalledWith(this.Subscription.findOne, expectedQuery) + expect(subscription).to.equal(this.subscription) + expect(subscription.membersLimit).to.equal(10) + return done() + } + ) + }) + + it('get for admin', function(done) { + return this.UserMembershipHandler.getEntity( + this.fakeEntityId, + EntityConfigs.group, + { isAdmin: true }, + null, + (error, subscription) => { + should.not.exist(error) + const expectedQuery = { + groupPlan: true, + _id: this.fakeEntityId + } + assertCalledWith(this.Subscription.findOne, expectedQuery) + return done() + } + ) + }) + + it('get with staffAccess field', function(done) { + return this.UserMembershipHandler.getEntity( + this.fakeEntityId, + EntityConfigs.group, + { staffAccess: { institutionMetrics: true } }, + 'institutionMetrics', + (error, subscription) => { + should.not.exist(error) + const expectedQuery = { + groupPlan: true, + _id: this.fakeEntityId + } + assertCalledWith(this.Subscription.findOne, expectedQuery) + return done() + } + ) + }) + + return it('handle error', function(done) { + this.Subscription.findOne.yields(new Error('some error')) + return this.UserMembershipHandler.getEntity( + this.fakeEntityId, + EntityConfigs.group, + this.user._id, + null, + (error, subscription) => { + should.exist(error) + return done() + } + ) + }) + })) + + describe('getEntityWithoutAuthorizationCheck', function() { + it('get publisher', function(done) { + return this.UserMembershipHandler.getEntityWithoutAuthorizationCheck( + this.fakeEntityId, + EntityConfigs.publisher, + (error, subscription) => { + should.not.exist(error) + const expectedQuery = { slug: this.fakeEntityId } + assertCalledWith(this.Publisher.findOne, expectedQuery) + expect(subscription).to.equal(this.publisher) + return done() + } + ) + }) + + describe('institutions', function() { + it('get institution', function(done) { + return this.UserMembershipHandler.getEntity( + this.institution.v1Id, + EntityConfigs.institution, + this.user, + null, + (error, institution) => { + should.not.exist(error) + const expectedQuery = { + v1Id: this.institution.v1Id, + managerIds: ObjectId(this.user._id) + } + assertCalledWith(this.Institution.findOne, expectedQuery) + expect(institution).to.equal(this.institution) + return done() + } + ) + }) + + return it('handle errors', function(done) { + this.Institution.findOne.yields(new Error('nope')) + return this.UserMembershipHandler.getEntity( + this.fakeEntityId, + EntityConfigs.institution, + this.user._id, + null, + (error, institution) => { + should.exist(error) + expect(error).to.not.be.an.instanceof(Errors.NotFoundError) + return done() + } + ) + }) + }) + + return describe('publishers', () => + it('get publisher', function(done) { + return this.UserMembershipHandler.getEntity( + this.publisher.slug, + EntityConfigs.publisher, + this.user, + null, + (error, institution) => { + should.not.exist(error) + const expectedQuery = { + slug: this.publisher.slug, + managerIds: ObjectId(this.user._id) + } + assertCalledWith(this.Publisher.findOne, expectedQuery) + expect(institution).to.equal(this.publisher) + return done() + } + ) + })) + }) + + describe('getUsers', function() { + describe('group', () => + it('build view model for all users', function(done) { + return this.UserMembershipHandler.getUsers( + this.subscription, + EntityConfigs.group, + (error, users) => { + const expectedCallcount = + this.subscription.member_ids.length + + this.subscription.invited_emails.length + + this.subscription.teamInvites.length + expect(this.UserMembershipViewModel.buildAsync.callCount).to.equal( + expectedCallcount + ) + return done() + } + ) + })) + + describe('group mamagers', () => + it('build view model for all managers', function(done) { + return this.UserMembershipHandler.getUsers( + this.subscription, + EntityConfigs.groupManagers, + (error, users) => { + const expectedCallcount = this.subscription.manager_ids.length + expect(this.UserMembershipViewModel.buildAsync.callCount).to.equal( + expectedCallcount + ) + return done() + } + ) + })) + + return describe('institution', () => + it('build view model for all managers', function(done) { + return this.UserMembershipHandler.getUsers( + this.institution, + EntityConfigs.institution, + (error, users) => { + const expectedCallcount = this.institution.managerIds.length + expect(this.UserMembershipViewModel.buildAsync.callCount).to.equal( + expectedCallcount + ) + return done() + } + ) + })) + }) + + describe('createEntity', () => + it('creates publisher', function(done) { + return this.UserMembershipHandler.createEntity( + this.fakeEntityId, + EntityConfigs.publisher, + (error, publisher) => { + should.not.exist(error) + assertCalledWith(this.Publisher.create, { slug: this.fakeEntityId }) + return done() + } + ) + })) + + describe('addUser', function() { + beforeEach(function() { + return (this.email = this.newUser.email) + }) + + return describe('institution', function() { + it('get user', function(done) { + return this.UserMembershipHandler.addUser( + this.institution, + EntityConfigs.institution, + this.email, + (error, user) => { + assertCalledWith(this.UserGetter.getUserByAnyEmail, this.email) + return done() + } + ) + }) + + it('handle user not found', function(done) { + this.UserGetter.getUserByAnyEmail.yields(null, null) + return this.UserMembershipHandler.addUser( + this.institution, + EntityConfigs.institution, + this.email, + error => { + expect(error).to.exist + expect(error.userNotFound).to.equal(true) + return done() + } + ) + }) + + it('handle user already added', function(done) { + this.institution.managerIds.push(this.newUser._id) + return this.UserMembershipHandler.addUser( + this.institution, + EntityConfigs.institution, + this.email, + (error, users) => { + expect(error).to.exist + expect(error.alreadyAdded).to.equal(true) + return done() + } + ) + }) + + it('add user to institution', function(done) { + return this.UserMembershipHandler.addUser( + this.institution, + EntityConfigs.institution, + this.email, + (error, user) => { + assertCalledWith(this.institution.update, { + $addToSet: { managerIds: this.newUser._id } + }) + return done() + } + ) + }) + + return it('return user view', function(done) { + return this.UserMembershipHandler.addUser( + this.institution, + EntityConfigs.institution, + this.email, + (error, user) => { + user.should.equal(this.newUser) + return done() + } + ) + }) + }) + }) + + return describe('removeUser', () => + describe('institution', function() { + it('remove user from institution', function(done) { + return this.UserMembershipHandler.removeUser( + this.institution, + EntityConfigs.institution, + this.newUser._id, + (error, user) => { + const { lastCall } = this.institution.update + assertCalledWith(this.institution.update, { + $pull: { managerIds: this.newUser._id } + }) + return done() + } + ) + }) + + return it('handle admin', function(done) { + this.subscription.admin_id = this.newUser._id + return this.UserMembershipHandler.removeUser( + this.subscription, + EntityConfigs.groupManagers, + this.newUser._id, + (error, user) => { + expect(error).to.exist + expect(error.isAdmin).to.equal(true) + return done() + } + ) + }) + })) +}) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js b/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js new file mode 100644 index 0000000000..b40440df40 --- /dev/null +++ b/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js @@ -0,0 +1,138 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai') +const should = chai.should() +const { expect } = require('chai') +const sinon = require('sinon') +const assertCalledWith = sinon.assert.calledWith +const assertNotCalled = sinon.assert.notCalled +const mongojs = require('mongojs') +const { ObjectId } = mongojs +const modulePath = + '../../../../app/src/Features/UserMembership/UserMembershipViewModel' +const SandboxedModule = require('sandboxed-module') + +describe('UserMembershipViewModel', function() { + beforeEach(function() { + this.UserGetter = { getUserOrUserStubById: sinon.stub() } + this.UserMembershipViewModel = SandboxedModule.require(modulePath, { + requires: { + mongojs: mongojs, + '../User/UserGetter': this.UserGetter + } + }) + this.email = 'mock-email@bar.com' + this.user = { + _id: 'mock-user-id', + email: 'mock-email@baz.com', + first_name: 'Name' + } + return (this.userStub = { + _id: 'mock-user-stub-id', + email: 'mock-stub-email@baz.com' + }) + }) + + describe('build', function() { + it('build email', function() { + const viewModel = this.UserMembershipViewModel.build(this.email) + return expect(viewModel).to.deep.equal({ + email: this.email, + invite: true, + first_name: null, + last_name: null, + _id: null + }) + }) + + return it('build user', function() { + const viewModel = this.UserMembershipViewModel.build(this.user) + expect(viewModel._id).to.equal(this.user._id) + expect(viewModel.email).to.equal(this.user.email) + return expect(viewModel.invite).to.equal(false) + }) + }) + + return describe('build async', function() { + beforeEach(function() { + return (this.UserMembershipViewModel.build = sinon.stub()) + }) + + it('build email', function(done) { + return this.UserMembershipViewModel.buildAsync( + this.email, + (error, viewModel) => { + assertCalledWith(this.UserMembershipViewModel.build, this.email) + return done() + } + ) + }) + + it('build user', function(done) { + return this.UserMembershipViewModel.buildAsync( + this.user, + (error, viewModel) => { + assertCalledWith(this.UserMembershipViewModel.build, this.user) + return done() + } + ) + }) + + it('build user id', function(done) { + this.UserGetter.getUserOrUserStubById.yields(null, this.user, false) + return this.UserMembershipViewModel.buildAsync( + ObjectId(), + (error, viewModel) => { + should.not.exist(error) + assertNotCalled(this.UserMembershipViewModel.build) + expect(viewModel._id).to.equal(this.user._id) + expect(viewModel.email).to.equal(this.user.email) + expect(viewModel.first_name).to.equal(this.user.first_name) + expect(viewModel.invite).to.equal(false) + should.exist(viewModel.email) + return done() + } + ) + }) + + it('build user stub id', function(done) { + this.UserGetter.getUserOrUserStubById.yields(null, this.userStub, true) + return this.UserMembershipViewModel.buildAsync( + ObjectId(), + (error, viewModel) => { + should.not.exist(error) + assertNotCalled(this.UserMembershipViewModel.build) + expect(viewModel._id).to.equal(this.userStub._id) + expect(viewModel.email).to.equal(this.userStub.email) + expect(viewModel.invite).to.equal(true) + return done() + } + ) + }) + + return it('build user id with error', function(done) { + this.UserGetter.getUserOrUserStubById.yields(new Error('nope')) + const userId = ObjectId() + return this.UserMembershipViewModel.buildAsync( + userId, + (error, viewModel) => { + should.not.exist(error) + assertNotCalled(this.UserMembershipViewModel.build) + expect(viewModel._id).to.equal(userId.toString()) + should.not.exist(viewModel.email) + return done() + } + ) + }) + }) +}) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipsHandlerTests.js b/services/web/test/unit/src/UserMembership/UserMembershipsHandlerTests.js new file mode 100644 index 0000000000..24d5113d8a --- /dev/null +++ b/services/web/test/unit/src/UserMembership/UserMembershipsHandlerTests.js @@ -0,0 +1,66 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const assertCalledWith = sinon.assert.calledWith +const { ObjectId } = require('../../../../app/src/infrastructure/mongojs') +const modulePath = + '../../../../app/src/Features/UserMembership/UserMembershipsHandler' +const SandboxedModule = require('sandboxed-module') + +describe('UserMembershipsHandler', function() { + beforeEach(function() { + this.user = { _id: ObjectId() } + + this.Institution = { updateMany: sinon.stub().yields(null) } + this.Subscription = { updateMany: sinon.stub().yields(null) } + this.Publisher = { updateMany: sinon.stub().yields(null) } + return (this.UserMembershipsHandler = SandboxedModule.require(modulePath, { + requires: { + '../../models/Institution': { + Institution: this.Institution + }, + '../../models/Subscription': { + Subscription: this.Subscription + }, + '../../models/Publisher': { + Publisher: this.Publisher + } + } + })) + }) + + return describe('remove user', () => + it('remove user from all entities', function(done) { + return this.UserMembershipsHandler.removeUserFromAllEntities( + this.user._id, + error => { + assertCalledWith( + this.Institution.updateMany, + {}, + { $pull: { managerIds: this.user._id } } + ) + assertCalledWith( + this.Subscription.updateMany, + {}, + { $pull: { manager_ids: this.user._id } } + ) + assertCalledWith( + this.Publisher.updateMany, + {}, + { $pull: { managerIds: this.user._id } } + ) + return done() + } + ) + })) +}) diff --git a/services/web/test/unit/src/helpers/MockClient.js b/services/web/test/unit/src/helpers/MockClient.js new file mode 100644 index 0000000000..7b20f337ed --- /dev/null +++ b/services/web/test/unit/src/helpers/MockClient.js @@ -0,0 +1,35 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MockClient +const sinon = require('sinon') + +let idCounter = 0 + +module.exports = MockClient = class MockClient { + constructor() { + this.attributes = {} + this.join = sinon.stub() + this.emit = sinon.stub() + this.disconnect = sinon.stub() + this.id = idCounter++ + } + set(key, value, callback) { + this.attributes[key] = value + if (callback != null) { + return callback() + } + } + get(key, callback) { + return callback(null, this.attributes[key]) + } + disconnect() {} +} diff --git a/services/web/test/unit/src/helpers/MockRequest.js b/services/web/test/unit/src/helpers/MockRequest.js new file mode 100644 index 0000000000..11733fe679 --- /dev/null +++ b/services/web/test/unit/src/helpers/MockRequest.js @@ -0,0 +1,29 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +class MockRequest { + static initClass() { + this.prototype.session = { destroy() {} } + + this.prototype.params = {} + this.prototype.query = {} + this.prototype.body = {} + this.prototype._parsedUrl = {} + this.prototype.i18n = { + translate(str) { + return str + } + } + this.prototype.route = { path: '' } + } + param(param) { + return this.params[param] + } +} +MockRequest.initClass() + +module.exports = MockRequest diff --git a/services/web/test/unit/src/helpers/MockResponse.js b/services/web/test/unit/src/helpers/MockResponse.js new file mode 100644 index 0000000000..ee14ae720e --- /dev/null +++ b/services/web/test/unit/src/helpers/MockResponse.js @@ -0,0 +1,141 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') + +class MockResponse { + static initClass() { + this.prototype.setContentDisposition = sinon.stub() + + this.prototype.header = sinon.stub() + + this.prototype.contentType = sinon.stub() + } + constructor() { + this.rendered = false + this.redirected = false + this.returned = false + this.headers = {} + } + + render(template, variables) { + this.success = true + this.rendered = true + this.returned = true + this.renderedTemplate = template + this.renderedVariables = variables + if (this.callback != null) { + return this.callback() + } + } + + redirect(url) { + this.success = true + this.redirected = true + this.returned = true + this.redirectedTo = url + if (this.callback != null) { + return this.callback() + } + } + + sendStatus(status) { + if (arguments.length < 2) { + if (typeof status !== 'number') { + const body = status + status = 200 + } + } + this.statusCode = status + this.returned = true + if (status >= 200 && status < 300) { + this.success = true + } else { + this.success = false + } + if (this.callback != null) { + return this.callback() + } + } + + send(status, body) { + if (arguments.length < 2) { + if (typeof status !== 'number') { + body = status + status = 200 + } + } + this.statusCode = status + this.returned = true + if (status >= 200 && status < 300) { + this.success = true + } else { + this.success = false + } + if (body) { + this.body = body + } + if (this.callback != null) { + return this.callback() + } + } + + json(status, body) { + if (arguments.length < 2) { + if (typeof status !== 'number') { + body = status + status = this.statusCode || 200 + } + } + this.statusCode = status + this.returned = true + this.type = 'application/json' + if (status >= 200 && status < 300) { + this.success = true + } else { + this.success = false + } + if (body) { + this.body = JSON.stringify(body) + } + if (this.callback != null) { + return this.callback() + } + } + + status(status) { + this.statusCode = status + return this + } + + setHeader(header, value) { + return (this.headers[header] = value) + } + + setTimeout(timout) { + this.timout = timout + } + + end(data, encoding) { + if (this.callback) { + return this.callback() + } + } + + type(type) { + return (this.type = type) + } +} +MockResponse.initClass() + +module.exports = MockResponse diff --git a/services/web/test/unit/src/infrastructure/CsrfTests.js b/services/web/test/unit/src/infrastructure/CsrfTests.js new file mode 100644 index 0000000000..050f3a21c1 --- /dev/null +++ b/services/web/test/unit/src/infrastructure/CsrfTests.js @@ -0,0 +1,176 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { assert } = require('chai') +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/infrastructure/Csrf.js' +const SandboxedModule = require('sandboxed-module') + +describe('Csrf', function() { + beforeEach(function() { + this.csurf_csrf = sinon + .stub() + .callsArgWith(2, (this.err = { code: 'EBADCSRFTOKEN' })) + this.Csrf = SandboxedModule.require(modulePath, { + requires: { + csurf: sinon.stub().returns(this.csurf_csrf) + } + }) + this.csrf = new this.Csrf() + this.next = sinon.stub() + this.path = '/foo/bar' + this.req = { + path: this.path, + method: 'POST' + } + return (this.res = {}) + }) + + describe('the middleware', function() { + describe('when there are no excluded routes', () => + it('passes the csrf error on', function() { + this.csrf.middleware(this.req, this.res, this.next) + return expect(this.next.calledWith(this.err)).to.equal(true) + })) + + describe('when the route is excluded', () => + it('does not pass the csrf error on', function() { + this.csrf.disableDefaultCsrfProtection(this.path, 'POST') + this.csrf.middleware(this.req, this.res, this.next) + return expect(this.next.calledWith(this.err)).to.equal(false) + })) + + describe('when there is a partial route match', function() { + it('passes the csrf error on when the match is too short', function() { + this.csrf.disableDefaultCsrfProtection('/foo', 'POST') + this.csrf.middleware(this.req, this.res, this.next) + return expect(this.next.calledWith(this.err)).to.equal(true) + }) + + return it('passes the csrf error on when the match is too long', function() { + this.csrf.disableDefaultCsrfProtection('/foo/bar/baz', 'POST') + this.csrf.middleware(this.req, this.res, this.next) + return expect(this.next.calledWith(this.err)).to.equal(true) + }) + }) + + describe('when there are multiple exclusions', function() { + it('does not pass the csrf error on when the match is present', function() { + this.csrf.disableDefaultCsrfProtection(this.path, 'POST') + this.csrf.disableDefaultCsrfProtection('/test', 'POST') + this.csrf.disableDefaultCsrfProtection('/a/b/c', 'POST') + this.csrf.middleware(this.req, this.res, this.next) + return expect(this.next.calledWith(this.err)).to.equal(false) + }) + + return it('passes the csrf error on when the match is not present', function() { + this.csrf.disableDefaultCsrfProtection('/url', 'POST') + this.csrf.disableDefaultCsrfProtection('/test', 'POST') + this.csrf.disableDefaultCsrfProtection('/a/b/c', 'POST') + this.csrf.middleware(this.req, this.res, this.next) + return expect(this.next.calledWith(this.err)).to.equal(true) + }) + }) + + describe('when the method does not match', () => + it('passes the csrf error on', function() { + this.csrf.disableDefaultCsrfProtection(this.path, 'POST') + this.req.method = 'GET' + this.csrf.middleware(this.req, this.res, this.next) + return expect(this.next.calledWith(this.err)).to.equal(true) + })) + + return describe('when the route is excluded, but the error is not a bad-csrf-token error', () => + it('passes the error on', function() { + let err + this.Csrf = SandboxedModule.require(modulePath, { + requires: { + csurf: (this.csurf = sinon + .stub() + .returns( + (this.csurf_csrf = sinon + .stub() + .callsArgWith(2, (err = { code: 'EOTHER' }))) + )) + } + }) + this.csrf = new this.Csrf() + this.csrf.disableDefaultCsrfProtection(this.path, 'POST') + this.csrf.middleware(this.req, this.res, this.next) + expect(this.next.calledWith(err)).to.equal(true) + return expect(this.next.calledWith(this.err)).to.equal(false) + })) + }) + + describe('validateRequest', function() { + describe('when the request is invalid', () => + it('calls the callback with `false`', function() { + this.cb = sinon.stub() + this.Csrf.validateRequest(this.req, this.cb) + return expect(this.cb.calledWith(false)).to.equal(true) + })) + + return describe('when the request is valid', () => + it('calls the callback with `true`', function() { + this.Csrf = SandboxedModule.require(modulePath, { + requires: { + csurf: (this.csurf = sinon + .stub() + .returns((this.csurf_csrf = sinon.stub().callsArg(2)))) + } + }) + this.cb = sinon.stub() + this.Csrf.validateRequest(this.req, this.cb) + return expect(this.cb.calledWith(true)).to.equal(true) + })) + }) + + return describe('validateToken', function() { + describe('when the request is invalid', () => + it('calls the callback with `false`', function() { + this.cb = sinon.stub() + this.Csrf.validateToken('token', {}, this.cb) + return expect(this.cb.calledWith(false)).to.equal(true) + })) + + describe('when the request is valid', () => + it('calls the callback with `true`', function() { + this.Csrf = SandboxedModule.require(modulePath, { + requires: { + csurf: (this.csurf = sinon + .stub() + .returns((this.csurf_csrf = sinon.stub().callsArg(2)))) + } + }) + this.cb = sinon.stub() + this.Csrf.validateToken('goodtoken', {}, this.cb) + return expect(this.cb.calledWith(true)).to.equal(true) + })) + + return describe('when there is no token', () => + it('calls the callback with `false`', function() { + this.Csrf = SandboxedModule.require(modulePath, { + requires: { + csurf: (this.csurf = sinon + .stub() + .returns((this.csurf_csrf = sinon.stub().callsArg(2)))) + } + }) + this.cb = sinon.stub() + this.Csrf.validateToken(null, {}, this.cb) + return expect(this.cb.calledWith(false)).to.equal(true) + })) + }) +}) diff --git a/services/web/test/unit/src/infrastructure/GeoIpLookupTests.js b/services/web/test/unit/src/infrastructure/GeoIpLookupTests.js new file mode 100644 index 0000000000..6e939d97a5 --- /dev/null +++ b/services/web/test/unit/src/infrastructure/GeoIpLookupTests.js @@ -0,0 +1,199 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join( + __dirname, + '../../../../app/src/infrastructure/GeoIpLookup' +) +const { expect } = require('chai') + +describe('GeoIpLookup', function() { + beforeEach(function() { + this.settings = { + apis: { + geoIpLookup: { + url: 'http://lookup.com' + } + } + } + this.request = { get: sinon.stub() } + this.GeoIpLookup = SandboxedModule.require(modulePath, { + requires: { + request: this.request, + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {}, + err() {} + } + } + }) + this.ipAddress = '123.456.789.123' + + return (this.stubbedResponse = { + ip: this.ipAddress, + country_code: 'GB', + country_name: 'United Kingdom', + region_code: 'H9', + region_name: 'London, City of', + city: 'London', + zipcode: 'SE16', + latitude: 51.0, + longitude: -0.0493, + metro_code: '', + area_code: '' + }) + }) + + describe('getDetails', function() { + beforeEach(function() { + return this.request.get.callsArgWith(1, null, null, this.stubbedResponse) + }) + + it('should request the details using the ip', function(done) { + return this.GeoIpLookup.getDetails(this.ipAddress, err => { + this.request.get + .calledWith({ + url: this.settings.apis.geoIpLookup.url + '/' + this.ipAddress, + timeout: 1000, + json: true + }) + .should.equal(true) + return done() + }) + }) + + it('should return the ip details', function(done) { + return this.GeoIpLookup.getDetails( + this.ipAddress, + (err, returnedDetails) => { + assert.deepEqual(returnedDetails, this.stubbedResponse) + return done() + } + ) + }) + + return it('should take the first ip in the string', function(done) { + return this.GeoIpLookup.getDetails( + ` ${this.ipAddress} 456.312.452.102 432.433.888.234`, + err => { + this.request.get + .calledWith({ + url: this.settings.apis.geoIpLookup.url + '/' + this.ipAddress, + timeout: 1000, + json: true + }) + .should.equal(true) + return done() + } + ) + }) + }) + + return describe('getCurrencyCode', function() { + it('should return GBP for GB country', function(done) { + this.GeoIpLookup.getDetails = sinon + .stub() + .callsArgWith(1, null, this.stubbedResponse) + return this.GeoIpLookup.getCurrencyCode(this.ipAddress, function( + err, + currencyCode + ) { + currencyCode.should.equal('GBP') + return done() + }) + }) + + it('should return GBP for gb country', function(done) { + this.stubbedResponse.country_code = 'gb' + this.GeoIpLookup.getDetails = sinon + .stub() + .callsArgWith(1, null, this.stubbedResponse) + return this.GeoIpLookup.getCurrencyCode(this.ipAddress, function( + err, + currencyCode + ) { + currencyCode.should.equal('GBP') + return done() + }) + }) + + it('should return USD for US', function(done) { + this.stubbedResponse.country_code = 'US' + this.GeoIpLookup.getDetails = sinon + .stub() + .callsArgWith(1, null, this.stubbedResponse) + return this.GeoIpLookup.getCurrencyCode(this.ipAddress, function( + err, + currencyCode + ) { + currencyCode.should.equal('USD') + return done() + }) + }) + + it('should return EUR for DE', function(done) { + this.stubbedResponse.country_code = 'DE' + this.GeoIpLookup.getDetails = sinon + .stub() + .callsArgWith(1, null, this.stubbedResponse) + return this.GeoIpLookup.getCurrencyCode(this.ipAddress, function( + err, + currencyCode + ) { + currencyCode.should.equal('EUR') + return done() + }) + }) + + it('should default to USD if there is an error', function(done) { + this.GeoIpLookup.getDetails = sinon.stub().callsArgWith(1, 'problem') + return this.GeoIpLookup.getCurrencyCode(this.ipAddress, function( + err, + currencyCode + ) { + currencyCode.should.equal('USD') + return done() + }) + }) + + it('should default to USD if there are no details', function(done) { + this.GeoIpLookup.getDetails = sinon.stub().callsArgWith(1) + return this.GeoIpLookup.getCurrencyCode(this.ipAddress, function( + err, + currencyCode + ) { + currencyCode.should.equal('USD') + return done() + }) + }) + + return it('should default to USD if there is no match for their country', function(done) { + this.stubbedResponse.country_code = 'Non existant' + this.GeoIpLookup.getDetails = sinon + .stub() + .callsArgWith(1, null, this.stubbedResponse) + return this.GeoIpLookup.getCurrencyCode(this.ipAddress, function( + err, + currencyCode + ) { + currencyCode.should.equal('USD') + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/infrastructure/LockManager/ReleasingTheLock.js b/services/web/test/unit/src/infrastructure/LockManager/ReleasingTheLock.js new file mode 100644 index 0000000000..9f0d1c1564 --- /dev/null +++ b/services/web/test/unit/src/infrastructure/LockManager/ReleasingTheLock.js @@ -0,0 +1,50 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const assert = require('assert') +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../../app/src/infrastructure/LockManager.js' +) +const lockKey = `lock:web:{${5678}}` +const lockValue = '123456' +const SandboxedModule = require('sandboxed-module') + +describe('LockManager - releasing the lock', function() { + const deleteStub = sinon.stub().callsArgWith(4) + const mocks = { + 'logger-sharelatex': { + log() {} + }, + + './RedisWrapper': { + client() { + return { + auth() {}, + eval: deleteStub + } + } + } + } + + const LockManager = SandboxedModule.require(modulePath, { requires: mocks }) + LockManager.unlockScript = 'this is the unlock script' + + return it('should put a all data into memory', done => + LockManager._releaseLock(lockKey, lockValue, function() { + deleteStub + .calledWith(LockManager.unlockScript, 1, lockKey, lockValue) + .should.equal(true) + return done() + })) +}) diff --git a/services/web/test/unit/src/infrastructure/LockManager/getLockTests.js b/services/web/test/unit/src/infrastructure/LockManager/getLockTests.js new file mode 100644 index 0000000000..a726e91ef6 --- /dev/null +++ b/services/web/test/unit/src/infrastructure/LockManager/getLockTests.js @@ -0,0 +1,175 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../../app/src/infrastructure/LockManager.js' +) +const SandboxedModule = require('sandboxed-module') + +describe('LockManager - getting the lock', function() { + beforeEach(function() { + this.LockManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {} + }, + './RedisWrapper': { + client() { + return { auth() {} } + } + }, + 'settings-sharelatex': { redis: {} }, + 'metrics-sharelatex': { + inc() {}, + gauge() {} + } + } + }) + + this.callback = sinon.stub() + this.key = 'lock:web:lockName:project-id}' + return (this.namespace = 'lockName') + }) + + describe('when the lock is not set', function() { + beforeEach(function(done) { + this.LockManager._tryLock = sinon.stub().yields(null, true) + return this.LockManager._getLock(this.key, this.namespace, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) + + it('should try to get the lock', function() { + return this.LockManager._tryLock + .calledWith(this.key, this.namespace) + .should.equal(true) + }) + + it('should only need to try once', function() { + return this.LockManager._tryLock.callCount.should.equal(1) + }) + + return it('should return the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the lock is initially set', function() { + beforeEach(function(done) { + const startTime = Date.now() + let tries = 0 + this.LockManager.LOCK_TEST_INTERVAL = 5 + this.LockManager._tryLock = function(key, namespace, callback) { + if (callback == null) { + callback = function(error, isFree) {} + } + if (Date.now() - startTime < 20 || tries < 2) { + tries = tries + 1 + return callback(null, false) + } else { + return callback(null, true) + } + } + sinon.spy(this.LockManager, '_tryLock') + + return this.LockManager._getLock(this.key, this.namespace, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) + + it('should call tryLock multiple times until free', function() { + return (this.LockManager._tryLock.callCount > 1).should.equal(true) + }) + + return it('should return the callback', function() { + return this.callback.calledWith(null).should.equal(true) + }) + }) + + describe('when the lock times out', function() { + beforeEach(function(done) { + const time = Date.now() + this.LockManager.MAX_LOCK_WAIT_TIME = 5 + this.LockManager._tryLock = sinon.stub().yields(null, false) + return this.LockManager._getLock(this.key, this.namespace, (...args) => { + this.callback(...Array.from(args || [])) + return done() + }) + }) + + return it('should return the callback with an error', function() { + return this.callback.calledWith(new Error('timeout')).should.equal(true) + }) + }) + + return describe('when there are multiple requests for the same lock', function() { + beforeEach(function(done) { + let locked = false + this.results = [] + this.LockManager.LOCK_TEST_INTERVAL = 1 + this.LockManager._tryLock = function(key, namespace, callback) { + if (callback == null) { + callback = function(error, gotLock, lockValue) {} + } + if (locked) { + return callback(null, false) + } else { + locked = true // simulate getting the lock + return callback(null, true) + } + } + // Start ten lock requests in order at 1ms 2ms 3ms... + // with them randomly holding the lock for 0-100ms. + // Use predefined values for the random delay to make the test + // deterministic. + const randomDelays = [52, 45, 41, 84, 60, 81, 31, 46, 9, 43] + let startTime = 0 + return Array.from(randomDelays).map((randomDelay, i) => + ((randomDelay, i) => { + startTime += 1 + return setTimeout(() => { + // changing the next line to the old method of LockManager._getLockByPolling + // should give results in a random order and cause the test to fail. + return this.LockManager._getLock( + this.key, + this.namespace, + (...args) => { + setTimeout( + () => (locked = false), // release the lock after a random amount of time + randomDelay + ) + this.results.push(i) + if (this.results.length === 10) { + return done() + } + } + ) + }, startTime) + })(randomDelay, i) + ) + }) + + return it('should process the requests in order', function() { + return this.results.should.deep.equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + }) + }) +}) diff --git a/services/web/test/unit/src/infrastructure/LockManager/tryLockTests.js b/services/web/test/unit/src/infrastructure/LockManager/tryLockTests.js new file mode 100644 index 0000000000..1d1b191292 --- /dev/null +++ b/services/web/test/unit/src/infrastructure/LockManager/tryLockTests.js @@ -0,0 +1,77 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const path = require('path') +const modulePath = path.join( + __dirname, + '../../../../../app/src/infrastructure/LockManager.js' +) +const SandboxedModule = require('sandboxed-module') + +describe('LockManager - trying the lock', function() { + beforeEach(function() { + this.LockManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': { + log() {} + }, + './RedisWrapper': { + client: () => { + return { + auth() {}, + set: (this.set = sinon.stub()) + } + } + }, + 'settings-sharelatex': { redis: {} }, + 'metrics-sharelatex': { + inc() {} + } + } + }) + this.callback = sinon.stub() + this.key = 'lock:web:lockName:project-id}' + return (this.namespace = 'lockName') + }) + + describe('when the lock is not set', function() { + beforeEach(function() { + this.set.callsArgWith(5, null, 'OK') + this.LockManager.randomLock = sinon.stub().returns('random-lock-value') + return this.LockManager._tryLock(this.key, this.namespace, this.callback) + }) + + it('should set the lock key with an expiry if it is not set', function() { + return this.set + .calledWith(this.key, 'random-lock-value', 'EX', 30, 'NX') + .should.equal(true) + }) + + return it('should return the callback with true', function() { + return this.callback.calledWith(null, true).should.equal(true) + }) + }) + + return describe('when the lock is already set', function() { + beforeEach(function() { + this.set.callsArgWith(5, null, null) + return this.LockManager._tryLock(this.key, this.namespace, this.callback) + }) + + return it('should return the callback with false', function() { + return this.callback.calledWith(null, false).should.equal(true) + }) + }) +}) diff --git a/services/web/test/unit/src/infrastructure/ProxyManagerTests.js b/services/web/test/unit/src/infrastructure/ProxyManagerTests.js new file mode 100644 index 0000000000..166ce5946a --- /dev/null +++ b/services/web/test/unit/src/infrastructure/ProxyManagerTests.js @@ -0,0 +1,210 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const sinon = require('sinon') +const assertCalledWith = sinon.assert.calledWith +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/infrastructure/ProxyManager' +const SandboxedModule = require('sandboxed-module') +const MockRequest = require('../helpers/MockRequest') +const MockResponse = require('../helpers/MockResponse') + +describe('ProxyManager', function() { + before(function() { + this.settings = { proxyUrls: {} } + this.request = sinon.stub().returns({ + on() {}, + pipe() {} + }) + this.proxyManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { + log() {} + }, + request: this.request + } + }) + this.proxyPath = '/foo/bar' + this.req = new MockRequest() + this.res = new MockResponse() + return (this.next = sinon.stub()) + }) + + describe('apply', function() { + it('applies all paths', function() { + this.router = { get: sinon.stub() } + this.settings.proxyUrls = { + '/foo/bar': '', + '/foo/:id': '' + } + this.proxyManager.apply(this.router) + sinon.assert.calledTwice(this.router.get) + assertCalledWith(this.router.get, '/foo/bar') + return assertCalledWith(this.router.get, '/foo/:id') + }) + + return it('applies methods other than get', function() { + this.router = { + post: sinon.stub(), + put: sinon.stub() + } + this.settings.proxyUrls = { + '/foo/bar': { options: { method: 'post' } }, + '/foo/:id': { options: { method: 'put' } } + } + this.proxyManager.apply(this.router) + sinon.assert.calledOnce(this.router.post) + sinon.assert.calledOnce(this.router.put) + assertCalledWith(this.router.post, '/foo/bar') + return assertCalledWith(this.router.put, '/foo/:id') + }) + }) + + return describe('createProxy', function() { + beforeEach(function() { + this.req.url = this.proxyPath + this.req.route.path = this.proxyPath + this.req.query = {} + this.req.params = {} + this.req.headers = {} + return (this.settings.proxyUrls = {}) + }) + + afterEach(function() { + this.next.reset() + return this.request.reset() + }) + + it('does not calls next when match', function() { + const target = '/' + this.settings.proxyUrls[this.proxyPath] = target + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + sinon.assert.notCalled(this.next) + return sinon.assert.called(this.request) + }) + + it('proxy full URL', function() { + const targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash' + this.settings.proxyUrls[this.proxyPath] = targetUrl + this.proxyManager.createProxy(targetUrl)(this.req) + return assertCalledWith(this.request, { url: targetUrl }) + }) + + it('overwrite query', function() { + const targetUrl = 'foo.bar/baz?query' + this.req.query = { requestQuery: 'important' } + this.settings.proxyUrls[this.proxyPath] = targetUrl + this.proxyManager.createProxy(targetUrl)(this.req) + const newTargetUrl = 'foo.bar/baz?requestQuery=important' + return assertCalledWith(this.request, { url: newTargetUrl }) + }) + + it('handles target objects', function() { + const target = { baseUrl: 'api.v1', path: '/pa/th' } + this.settings.proxyUrls[this.proxyPath] = target + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + return assertCalledWith(this.request, { url: 'api.v1/pa/th' }) + }) + + it('handles missing baseUrl', function() { + const target = { path: '/pa/th' } + this.settings.proxyUrls[this.proxyPath] = target + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + return assertCalledWith(this.request, { url: 'undefined/pa/th' }) + }) + + it('handles dynamic path', function() { + const target = { + baseUrl: 'api.v1', + path(params) { + return `/resource/${params.id}` + } + } + this.settings.proxyUrls['/res/:id'] = target + this.req.url = '/res/123' + this.req.route.path = '/res/:id' + this.req.params = { id: 123 } + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + return assertCalledWith(this.request, { url: 'api.v1/resource/123' }) + }) + + it('set arbitrary options on request', function() { + const target = { + baseUrl: 'api.v1', + path: '/foo', + options: { foo: 'bar' } + } + this.req.url = '/foo' + this.req.route.path = '/foo' + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + return assertCalledWith(this.request, { + foo: 'bar', + url: 'api.v1/foo' + }) + }) + + it('passes cookies', function() { + const target = { baseUrl: 'api.v1', path: '/foo' } + this.req.url = '/foo' + this.req.route.path = '/foo' + this.req.headers = { cookie: 'cookie' } + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + return assertCalledWith(this.request, { + headers: { + Cookie: 'cookie' + }, + url: 'api.v1/foo' + }) + }) + + it('passes body for post', function() { + const target = { + baseUrl: 'api.v1', + path: '/foo', + options: { method: 'post' } + } + this.req.url = '/foo' + this.req.route.path = '/foo' + this.req.body = { foo: 'bar' } + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + return assertCalledWith(this.request, { + form: { + foo: 'bar' + }, + method: 'post', + url: 'api.v1/foo' + }) + }) + + return it('passes body for put', function() { + const target = { + baseUrl: 'api.v1', + path: '/foo', + options: { method: 'put' } + } + this.req.url = '/foo' + this.req.route.path = '/foo' + this.req.body = { foo: 'bar' } + this.proxyManager.createProxy(target)(this.req, this.res, this.next) + return assertCalledWith(this.request, { + form: { + foo: 'bar' + }, + method: 'put', + url: 'api.v1/foo' + }) + }) + }) +}) diff --git a/services/web/test/unit/src/infrastructure/RateLimterTests.js b/services/web/test/unit/src/infrastructure/RateLimterTests.js new file mode 100644 index 0000000000..42064a0338 --- /dev/null +++ b/services/web/test/unit/src/infrastructure/RateLimterTests.js @@ -0,0 +1,159 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { assert } = require('chai') +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/infrastructure/RateLimiter.js' +const SandboxedModule = require('sandboxed-module') + +describe('RateLimiter', function() { + beforeEach(function() { + this.settings = { + redis: { + web: { + port: '1234', + host: 'somewhere', + password: 'password' + } + } + } + this.rclient = { + incr: sinon.stub(), + get: sinon.stub(), + expire: sinon.stub(), + exec: sinon.stub() + } + this.rclient.multi = sinon.stub().returns(this.rclient) + this.RedisWrapper = { client: sinon.stub().returns(this.rclient) } + + this.endpointName = 'compiles' + this.subject = 'some-project-id' + this.timeInterval = 20 + this.throttleLimit = 5 + + this.requires = { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + err: sinon.stub() + }), + 'metrics-sharelatex': (this.Metrics = { inc: sinon.stub() }), + './RedisWrapper': this.RedisWrapper + } + + this.details = { + endpointName: this.endpointName, + subjectName: this.subject, + throttle: this.throttleLimit, + timeInterval: this.timeInterval + } + return (this.key = `RateLimiter:${this.endpointName}:{${this.subject}}`) + }) + + describe('when action is permitted', function() { + beforeEach(function() { + this.requires['rolling-rate-limiter'] = opts => { + return sinon.stub().callsArgWith(1, null, 0, 22) + } + return (this.limiter = SandboxedModule.require(modulePath, { + requires: this.requires + })) + }) + + it('should not produce and error', function(done) { + return this.limiter.addCount({}, function(err, should) { + expect(err).to.equal(null) + return done() + }) + }) + + it('should callback with true', function(done) { + return this.limiter.addCount({}, function(err, should) { + expect(should).to.equal(true) + return done() + }) + }) + + return it('should not increment the metric', function(done) { + return this.limiter.addCount( + { endpointName: this.endpointName }, + (err, should) => { + sinon.assert.notCalled(this.Metrics.inc) + return done() + } + ) + }) + }) + + describe('when action is not permitted', function() { + beforeEach(function() { + this.requires['rolling-rate-limiter'] = opts => { + return sinon.stub().callsArgWith(1, null, 4000, 0) + } + return (this.limiter = SandboxedModule.require(modulePath, { + requires: this.requires + })) + }) + + it('should not produce and error', function(done) { + return this.limiter.addCount({}, function(err, should) { + expect(err).to.equal(null) + return done() + }) + }) + + it('should callback with false', function(done) { + return this.limiter.addCount({}, function(err, should) { + expect(should).to.equal(false) + return done() + }) + }) + + return it('should increment the metric', function(done) { + return this.limiter.addCount( + { endpointName: this.endpointName }, + (err, should) => { + sinon.assert.calledWith( + this.Metrics.inc, + `rate-limit-hit.${this.endpointName}`, + 1, + { path: this.endpointName } + ) + return done() + } + ) + }) + }) + + return describe('when limiter produces an error', function() { + beforeEach(function() { + this.requires['rolling-rate-limiter'] = opts => { + return sinon.stub().callsArgWith(1, new Error('woops')) + } + return (this.limiter = SandboxedModule.require(modulePath, { + requires: this.requires + })) + }) + + return it('should produce and error', function(done) { + return this.limiter.addCount({}, function(err, should) { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + return done() + }) + }) + }) +}) diff --git a/services/web/test/unit/src/infrastructure/RedisWrapperTests.js b/services/web/test/unit/src/infrastructure/RedisWrapperTests.js new file mode 100644 index 0000000000..c785941ed4 --- /dev/null +++ b/services/web/test/unit/src/infrastructure/RedisWrapperTests.js @@ -0,0 +1,62 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { assert } = require('chai') +const sinon = require('sinon') +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const modulePath = '../../../../app/src/infrastructure/RedisWrapper.js' +const SandboxedModule = require('sandboxed-module') + +describe('RedisWrapper', function() { + beforeEach(function() { + this.settings = { redis: {} } + this.redis = { createClient: sinon.stub() } + return (this.RedisWrapper = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'redis-sharelatex': this.redis + } + })) + }) + + return describe('client', function() { + it('should use the feature settings if present', function() { + this.settings.redis = { + my_feature: { + port: '23456', + host: 'otherhost', + password: 'banana' + } + } + this.RedisWrapper.client('my_feature') + return this.redis.createClient + .calledWith(this.settings.redis.my_feature) + .should.equal(true) + }) + + return it('should use the web settings if feature not present', function() { + this.settings.redis = { + web: { + port: '43', + host: 'otherhost', + password: 'banana' + } + } + this.RedisWrapper.client('my_feature') + return this.redis.createClient + .calledWith(this.settings.redis.web) + .should.equal(true) + }) + }) +})