Convert to ESM

GitOrigin-RevId: bc407a128b024792a65b62c2eaeacefa48ecfe8d
This commit is contained in:
Andrew Rumble
2025-10-16 08:06:56 +00:00
committed by Copybot
parent fb2d86ebd4
commit 9c557a0e78
21 changed files with 350 additions and 385 deletions
@@ -1,5 +1,5 @@
const bodyParser = require('body-parser')
const HttpErrorHandler = require('../Features/Errors/HttpErrorHandler')
import bodyParser from 'body-parser'
import HttpErrorHandler from '../Features/Errors/HttpErrorHandler.js'
function isBodyParserError(nextArg) {
if (nextArg instanceof Error) {
@@ -29,7 +29,7 @@ const wrapBodyParser = method => opts => {
}
}
module.exports = {
export default {
urlencoded: wrapBodyParser('urlencoded'),
json: wrapBodyParser('json'),
}
+7 -11
View File
@@ -1,7 +1,7 @@
const crypto = require('crypto')
const path = require('path')
import crypto from 'node:crypto'
import path from 'node:path'
module.exports = function ({
export default function ({
reportUri,
reportPercentage,
reportOnly = false,
@@ -57,7 +57,7 @@ module.exports = function ({
}
}
const buildDefaultPolicy = (reportUri, styleSrc) => {
export const buildDefaultPolicy = (reportUri, styleSrc) => {
const directives = [
`base-uri 'none'`, // forbid setting a "base" element
`default-src 'none'`, // forbid loading anything from a "src" attribute
@@ -104,7 +104,7 @@ const buildViewPolicy = (
return directives.join('; ')
}
const webRoot = path.resolve(__dirname, '..', '..', '..')
const webRoot = path.resolve(import.meta.dirname, '..', '..', '..')
// build the view path relative to the web root
function relativeViewPath(view) {
@@ -113,7 +113,7 @@ function relativeViewPath(view) {
: path.join('app', 'views', view)
}
function removeCSPHeaders(res) {
export function removeCSPHeaders(res) {
res.removeHeader('Content-Security-Policy')
res.removeHeader('Content-Security-Policy-Report-Only')
}
@@ -122,13 +122,9 @@ function removeCSPHeaders(res) {
* WARNING: allowing inline styles can open a security hole;
* this is intended only for use in specific circumstances, such as Safari's built-in PDF viewer.
*/
function allowUnsafeInlineStyles(res) {
export function allowUnsafeInlineStyles(res) {
res.set(
'Content-Security-Policy',
buildDefaultPolicy(undefined, "'unsafe-inline'")
)
}
module.exports.buildDefaultPolicy = buildDefaultPolicy
module.exports.removeCSPHeaders = removeCSPHeaders
module.exports.allowUnsafeInlineStyles = allowUnsafeInlineStyles
@@ -1,5 +1,5 @@
const Settings = require('@overleaf/settings')
const metrics = require('@overleaf/metrics')
import Settings from '@overleaf/settings'
import metrics from '@overleaf/metrics'
/**
* Middleware function to record session cookie metrics. This allows us to
@@ -27,4 +27,4 @@ function middleware(req, res, next) {
next()
}
module.exports = { middleware }
export default { middleware }
+13 -30
View File
@@ -1,22 +1,9 @@
/* 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
*/
import csurf from 'csurf'
import { promisify } from 'node:util'
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
const csurf = require('csurf')
const csrf = csurf()
const { promisify } = require('util')
const Settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
// Wrapper for `csurf` middleware that provides a list of routes that can be excluded from csrf checks.
//
@@ -53,7 +40,7 @@ class Csrf {
if (!this.excluded_routes[route]) {
this.excluded_routes[route] = {}
}
return (this.excluded_routes[route][method] = 1)
this.excluded_routes[route][method] = 1
}
middleware(req, res, next) {
@@ -62,21 +49,17 @@ class 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
) {
if (this.excluded_routes[req.path]?.[req.method] === 1) {
// ignore the error if it's due to a bad csrf token, and continue
return csrf(req, res, err => {
csrf(req, res, err => {
if (err && err.code !== 'EBADCSRFTOKEN') {
return next(err)
next(err)
} else {
return next()
next()
}
})
} else {
return csrf(req, res, next)
csrf(req, res, next)
}
}
@@ -85,7 +68,7 @@ class Csrf {
if (cb == null) {
cb = function (valid) {}
}
return csrf(req, null, err => cb(err))
csrf(req, null, err => cb(err))
}
static validateToken(token, session, cb) {
@@ -102,7 +85,7 @@ class Csrf {
method: 'POST',
session,
}
return Csrf.validateRequest(req, cb)
Csrf.validateRequest(req, cb)
}
}
@@ -111,4 +94,4 @@ Csrf.promises = {
validateToken: promisify(Csrf.validateToken),
}
module.exports = Csrf
export default Csrf
@@ -1,10 +1,11 @@
const session = require('express-session')
const RedisStore = require('connect-redis')(session)
const metrics = require('@overleaf/metrics')
const logger = require('@overleaf/logger')
const Settings = require('@overleaf/settings')
const SessionManager = require('../Features/Authentication/SessionManager')
const Metrics = require('@overleaf/metrics')
import session from 'express-session'
import RedisStoreFactory from 'connect-redis'
import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import SessionManager from '../Features/Authentication/SessionManager.js'
import Metrics from '@overleaf/metrics'
const RedisStore = RedisStoreFactory(session)
const MAX_SESSION_SIZE_THRESHOLD = 4096
@@ -37,14 +38,14 @@ class CustomSessionStore extends RedisStore {
}
const size = sess ? JSON.stringify(sess).length : 0
// record the number of redis session operations
metrics.inc('session.store.count', 1, {
Metrics.inc('session.store.count', 1, {
method,
type,
status: size > MAX_SESSION_SIZE_THRESHOLD ? 'oversize' : 'normal',
})
// record the redis session bandwidth for get/set operations
if (method === 'get' || method === 'set') {
metrics.count('session.store.bytes', size, { method, type })
Metrics.count('session.store.bytes', size, { method, type })
}
// log the largest anonymous session seen so far
if (type === 'anonymous' && size > CustomSessionStore.largestSessionSize) {
@@ -153,7 +154,7 @@ class CustomSetRedisClient {
set(args, cb) {
args.push(this.#flag)
this.#client.set(args, (err, ok) => {
metrics.inc('session.store.set', 1, {
Metrics.inc('session.store.set', 1, {
path: this.#flag,
status: err ? 'error' : ok ? 'success' : 'failure',
})
@@ -162,4 +163,4 @@ class CustomSetRedisClient {
}
}
module.exports = CustomSessionStore
export default CustomSessionStore
@@ -1,36 +1,43 @@
const logger = require('@overleaf/logger')
const Metrics = require('@overleaf/metrics')
const Settings = require('@overleaf/settings')
const _ = require('lodash')
const { URL } = require('url')
const Path = require('path')
const moment = require('moment')
const { fetchJson } = require('@overleaf/fetch-utils')
const contentDisposition = require('content-disposition')
const Features = require('./Features')
const SessionManager = require('../Features/Authentication/SessionManager')
const PackageVersions = require('./PackageVersions')
const Modules = require('./Modules')
const Errors = require('../Features/Errors/Errors')
const {
import logger from '@overleaf/logger'
import Metrics from '@overleaf/metrics'
import Settings from '@overleaf/settings'
import _ from 'lodash'
import { URL } from 'node:url'
import Path from 'node:path'
import moment from 'moment'
import { fetchJson } from '@overleaf/fetch-utils'
import contentDisposition from 'content-disposition'
import Features from './Features.js'
import SessionManager from '../Features/Authentication/SessionManager.js'
import PackageVersions from './PackageVersions.js'
import Modules from './Modules.js'
import Errors from '../Features/Errors/Errors.js'
import {
canRedirectToAdminDomain,
hasAdminAccess,
useAdminCapabilities,
useHasAdminCapability,
} = require('../Features/Helpers/AdminAuthorizationHelper')
const {
addOptionalCleanupHandlerAfterDrainingConnections,
} = require('./GracefulShutdown')
const { sanitizeSessionUserForFrontEnd } = require('./FrontEndUser')
} from '../Features/Helpers/AdminAuthorizationHelper.js'
import { addOptionalCleanupHandlerAfterDrainingConnections } from './GracefulShutdown.js'
import { sanitizeSessionUserForFrontEnd } from './FrontEndUser.mjs'
import { expressify } from '@overleaf/promise-utils'
const IEEE_BRAND_ID = Settings.ieeeBrandId
let webpackManifest
function loadManifest() {
async function loadManifest() {
switch (process.env.NODE_ENV) {
case 'production':
/* eslint-disable import/no-unresolved */
// Only load webpack manifest file in production.
webpackManifest = require('../../../public/manifest.json')
webpackManifest = (
await import('../../../public/manifest.json', {
with: { type: 'json' },
})
).default
/* eslint-enable import/no-unresolved */
break
case 'development': {
// In dev, fetch the manifest from the webpack container.
@@ -76,8 +83,8 @@ function getWebpackAssets(entrypoint, section) {
return webpackManifest.entrypoints[entrypoint].assets[section] || []
}
module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
loadManifest()
export default async function (webRouter, privateApiRouter, publicApiRouter) {
await loadManifest()
if (process.env.NODE_ENV === 'development') {
// In the dev-env, delay requests until we fetched the manifest once.
webRouter.use(function (req, res, next) {
@@ -253,10 +260,14 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
next()
})
webRouter.use(function (req, res, next) {
res.locals.StringHelper = require('../Features/Helpers/StringHelper')
next()
})
webRouter.use(
expressify(async function (req, res, next) {
res.locals.StringHelper = (
await import('../Features/Helpers/StringHelper.js')
).default
next()
})
)
webRouter.use(function (req, res, next) {
res.locals.csrfToken = req != null ? req.csrfToken() : undefined
@@ -1,4 +1,4 @@
function sanitizeSessionUserForFrontEnd(sessionUser) {
export function sanitizeSessionUserForFrontEnd(sessionUser) {
if (sessionUser != null) {
return {
email: sessionUser.email,
@@ -9,7 +9,3 @@ function sanitizeSessionUserForFrontEnd(sessionUser) {
return null
}
module.exports = {
sanitizeSessionUserForFrontEnd,
}
@@ -1,6 +1,6 @@
const settings = require('@overleaf/settings')
const logger = require('@overleaf/logger')
const { fetchJson } = require('@overleaf/fetch-utils')
import settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import { fetchJson } from '@overleaf/fetch-utils'
const DEFAULT_CURRENCY_CODE = 'USD'
@@ -104,7 +104,7 @@ async function getCurrencyCode(ip) {
return { currencyCode, countryCode }
}
module.exports = {
export default {
isValidCurrencyParam,
promises: {
getDetails,
@@ -1,6 +1,6 @@
const { callbackify, promisify } = require('util')
const JWT = require('jsonwebtoken')
const Settings = require('@overleaf/settings')
import { callbackify, promisify } from 'node:util'
import JWT from 'jsonwebtoken'
import Settings from '@overleaf/settings'
const jwtSign = promisify(JWT.sign)
@@ -20,7 +20,7 @@ function getDecoded(token) {
return decoded
}
module.exports = {
export default {
sign: callbackify(sign),
getDecoded,
promises: {
+1 -1
View File
@@ -1,6 +1,6 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
module.exports = {
export default {
queue: {
web_to_tpds_http_requests: 'web_to_tpds_http_requests',
tpds_to_web_http_requests: 'tpds_to_web_http_requests',
@@ -6,7 +6,7 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
module.exports = {
export default {
user(user) {
if (user == null) {
return null
@@ -1,16 +1,18 @@
const Features = require('./Features')
const Queues = require('./Queues')
const UserOnboardingEmailManager = require('../Features/User/UserOnboardingEmailManager')
const UserPostRegistrationAnalyticsManager = require('../Features/User/UserPostRegistrationAnalyticsManager')
const FeaturesUpdater = require('../Features/Subscription/FeaturesUpdater')
const {
import Features from './Features.js'
import Queues from './Queues.js'
import UserOnboardingEmailManager from '../Features/User/UserOnboardingEmailManager.js'
import UserPostRegistrationAnalyticsManager from '../Features/User/UserPostRegistrationAnalyticsManager.js'
import FeaturesUpdater from '../Features/Subscription/FeaturesUpdater.js'
import {
addOptionalCleanupHandlerBeforeStoppingTraffic,
addRequiredCleanupHandlerBeforeDrainingConnections,
} = require('./GracefulShutdown')
const EmailHandler = require('../Features/Email/EmailHandler')
const logger = require('@overleaf/logger')
const OError = require('@overleaf/o-error')
const Modules = require('./Modules')
} from './GracefulShutdown.js'
import EmailHandler from '../Features/Email/EmailHandler.js'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
import Modules from './Modules.js'
/**
* @typedef {{
@@ -124,4 +126,4 @@ function registerCleanup(queue) {
// Disconnect from redis is scheduled in queue setup.
}
module.exports = { start, registerQueue }
export default { start, registerQueue }
@@ -12,11 +12,12 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let RedirectManager
const settings = require('@overleaf/settings')
const { URL } = require('url')
import settings from '@overleaf/settings'
import { URL } from 'node:url'
module.exports = RedirectManager = {
let RedirectManager
export default RedirectManager = {
apply(webRouter) {
return (() => {
const result = []
@@ -3,7 +3,7 @@ import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import Validation from './Validation.js'
import csp from './CSP.mjs'
import csp, { removeCSPHeaders } from './CSP.mjs'
import Router from '../router.mjs'
import helmet from 'helmet'
import UserSessionsRedis from '../Features/User/UserSessionsRedis.js'
@@ -125,7 +125,7 @@ webRouter.use(
fileURLToPath(new URL('../../../public', import.meta.url)),
{
maxAge: STATIC_CACHE_AGE,
setHeaders: csp.removeCSPHeaders,
setHeaders: removeCSPHeaders,
}
)
)
@@ -257,7 +257,7 @@ if (Settings.cookieRollingSession) {
}
webRouter.use(ReferalConnect.use)
expressLocals(webRouter, privateApiRouter, publicApiRouter)
await expressLocals(webRouter, privateApiRouter, publicApiRouter)
webRouter.use(SessionAutostartMiddleware.invokeCallbackMiddleware)
webRouter.use(function checkIfSiteClosed(req, res, next) {
@@ -1,5 +1,5 @@
const Settings = require('@overleaf/settings')
const OError = require('@overleaf/o-error')
import Settings from '@overleaf/settings'
import OError from '@overleaf/o-error'
const botUserAgents = [
'kube-probe',
@@ -125,4 +125,4 @@ class SessionAutostartMiddleware {
}
}
module.exports = SessionAutostartMiddleware
export default SessionAutostartMiddleware
@@ -1,13 +1,15 @@
const logger = require('@overleaf/logger')
const settings = require('@overleaf/settings')
const fs = require('fs')
const {
import logger from '@overleaf/logger'
import settings from '@overleaf/settings'
import fs from 'node:fs'
import {
addOptionalCleanupHandlerAfterDrainingConnections,
addRequiredCleanupHandlerBeforeDrainingConnections,
} = require('./GracefulShutdown')
const Features = require('./Features')
const UserHandler = require('../Features/User/UserHandler')
const metrics = require('@overleaf/metrics')
} from './GracefulShutdown.js'
import Features from './Features.js'
import UserHandler from '../Features/User/UserHandler.js'
import metrics from '@overleaf/metrics'
// Monitor a site maintenance file (e.g. /etc/site_status) periodically and
// close the site if the file contents contain the string "closed".
@@ -56,7 +58,7 @@ function publishActiveUsersMetric() {
.catch(error => logger.error({ error }, 'error counting active users'))
}
module.exports = {
export default {
initialise() {
if (settings.enabledServices.includes('web') && statusFile) {
logger.debug(
@@ -1,12 +1,12 @@
const i18n = require('i18next')
const fsBackend = require('i18next-fs-backend')
const middleware = require('i18next-http-middleware')
const path = require('path')
const Settings = require('@overleaf/settings')
const { URL } = require('url')
const pug = require('pug-runtime')
const logger = require('@overleaf/logger')
const SafeHTMLSubstitution = require('../Features/Helpers/SafeHTMLSubstitution')
import i18n from 'i18next'
import fsBackend from 'i18next-fs-backend'
import middleware from 'i18next-http-middleware'
import path from 'node:path'
import Settings from '@overleaf/settings'
import { URL } from 'node:url'
import pug from 'pug-runtime'
import logger from '@overleaf/logger'
import SafeHTMLSubstitution from '../Features/Helpers/SafeHTMLSubstitution.js'
const fallbackLanguageCode = Settings.i18n.defaultLng || 'en'
const availableLanguageCodes = []
@@ -35,7 +35,7 @@ if (!availableLanguageCodes.includes(fallbackLanguageCode)) {
if (process.argv.includes('--watch-locales')) {
// Dummy imports for setting up watching of locales files.
for (const lngCode of availableLanguageCodes) {
require(`../../../locales/${lngCode}.json`)
await import(`../../../locales/${lngCode}.json`, { with: { type: 'json' } })
}
}
@@ -44,7 +44,7 @@ i18n
.use(middleware.LanguageDetector)
.init({
backend: {
loadPath: path.join(__dirname, '../../../locales/__lng__.json'),
loadPath: path.join(import.meta.dirname, '../../../locales/__lng__.json'),
},
// still using the v3 plural suffixes
@@ -140,7 +140,7 @@ function setLangBasedOnDomainMiddleware(req, res, next) {
// in direct usage
i18n.translate = i18n.t
module.exports = {
export default {
i18nMiddleware: middleware.handle(i18n),
setLangBasedOnDomainMiddleware,
i18n,
@@ -1,7 +1,7 @@
const Bowser = require('bowser')
const Settings = require('@overleaf/settings')
const Url = require('url')
const { getSafeRedirectPath } = require('../Features/Helpers/UrlHelper')
import Bowser from 'bowser'
import Settings from '@overleaf/settings'
import Url from 'node:url'
import { getSafeRedirectPath } from '../Features/Helpers/UrlHelper.js'
function unsupportedBrowserMiddleware(req, res, next) {
if (!Settings.unsupportedBrowsers) return next()
@@ -44,7 +44,7 @@ function renderUnsupportedBrowserPage(req, res) {
res.render('general/unsupported-browser', { fromURL })
}
module.exports = {
export default {
renderUnsupportedBrowserPage,
unsupportedBrowserMiddleware,
}
@@ -1,192 +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 { assert, expect } = require('chai')
const sinon = require('sinon')
const modulePath = '../../../../app/src/infrastructure/Csrf.js'
const SandboxedModule = require('sandboxed-module')
import { beforeEach, describe, expect, it, vi } from 'vitest'
import sinon from 'sinon'
const modulePath = '../../../../app/src/infrastructure/Csrf.mjs'
describe('Csrf', function () {
beforeEach(function () {
this.csurf_csrf = sinon
beforeEach(async function (ctx) {
ctx.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,
.callsArgWith(2, (ctx.err = { code: 'EBADCSRFTOKEN' }))
vi.doMock('csurf', () => ({
default: sinon.stub().returns(ctx.csurf_csrf),
}))
ctx.Csrf = (await import(modulePath)).default
ctx.csrf = new ctx.Csrf()
ctx.next = sinon.stub()
ctx.path = '/foo/bar'
ctx.req = {
path: ctx.path,
method: 'POST',
}
return (this.res = {})
ctx.res = {}
})
describe('the middleware', function () {
describe('when there are no excluded routes', function () {
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)
it('passes the csrf error on', function (ctx) {
ctx.csrf.middleware(ctx.req, ctx.res, ctx.next)
return expect(ctx.next.calledWith(ctx.err)).to.equal(true)
})
})
describe('when the route is excluded', function () {
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)
it('does not pass the csrf error on', function (ctx) {
ctx.csrf.disableDefaultCsrfProtection(ctx.path, 'POST')
ctx.csrf.middleware(ctx.req, ctx.res, ctx.next)
return expect(ctx.next.calledWith(ctx.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)
it('passes the csrf error on when the match is too short', function (ctx) {
ctx.csrf.disableDefaultCsrfProtection('/foo', 'POST')
ctx.csrf.middleware(ctx.req, ctx.res, ctx.next)
return expect(ctx.next.calledWith(ctx.err)).to.equal(true)
})
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)
it('passes the csrf error on when the match is too long', function (ctx) {
ctx.csrf.disableDefaultCsrfProtection('/foo/bar/baz', 'POST')
ctx.csrf.middleware(ctx.req, ctx.res, ctx.next)
return expect(ctx.next.calledWith(ctx.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)
it('does not pass the csrf error on when the match is present', function (ctx) {
ctx.csrf.disableDefaultCsrfProtection(ctx.path, 'POST')
ctx.csrf.disableDefaultCsrfProtection('/test', 'POST')
ctx.csrf.disableDefaultCsrfProtection('/a/b/c', 'POST')
ctx.csrf.middleware(ctx.req, ctx.res, ctx.next)
return expect(ctx.next.calledWith(ctx.err)).to.equal(false)
})
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)
it('passes the csrf error on when the match is not present', function (ctx) {
ctx.csrf.disableDefaultCsrfProtection('/url', 'POST')
ctx.csrf.disableDefaultCsrfProtection('/test', 'POST')
ctx.csrf.disableDefaultCsrfProtection('/a/b/c', 'POST')
ctx.csrf.middleware(ctx.req, ctx.res, ctx.next)
return expect(ctx.next.calledWith(ctx.err)).to.equal(true)
})
})
describe('when the method does not match', function () {
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)
it('passes the csrf error on', function (ctx) {
ctx.csrf.disableDefaultCsrfProtection(ctx.path, 'POST')
ctx.req.method = 'GET'
ctx.csrf.middleware(ctx.req, ctx.res, ctx.next)
return expect(ctx.next.calledWith(ctx.err)).to.equal(true)
})
})
describe('when the route is excluded, but the error is not a bad-csrf-token error', function () {
it('passes the error on', function () {
let err
this.Csrf = SandboxedModule.require(modulePath, {
globals: {
console,
},
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)
it('passes the error on', async function (ctx) {
const err = { code: 'EOTHER' }
ctx.csurf_csrf.callsArgWith(2, err)
const csrf = new ctx.Csrf()
csrf.disableDefaultCsrfProtection(ctx.path, 'POST')
csrf.middleware(ctx.req, ctx.res, ctx.next)
expect(ctx.next.calledWith(err)).to.equal(true)
expect(ctx.next.calledWith(ctx.err)).to.equal(false)
})
})
})
describe('validateRequest', function () {
describe('when the request is invalid', function () {
it('calls the callback with error', function () {
this.cb = sinon.stub()
this.Csrf.validateRequest(this.req, this.cb)
return expect(this.cb.calledWith(this.err)).to.equal(true)
it('calls the callback with error', function (ctx) {
ctx.cb = sinon.stub()
ctx.Csrf.validateRequest(ctx.req, ctx.cb)
expect(ctx.cb.calledWith(ctx.err)).to.equal(true)
})
})
describe('when the request is valid', function () {
it('calls the callback without an error', function () {
this.Csrf = SandboxedModule.require(modulePath, {
globals: {
console,
},
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()).to.equal(true)
it('calls the callback without an error', async function (ctx) {
vi.doMock('csurf', () => ({
default: (ctx.csurf = sinon
.stub()
.returns((ctx.csurf_csrf = sinon.stub().callsArg(2)))),
}))
ctx.Csrf = (await import(modulePath)).default
ctx.cb = sinon.stub()
ctx.Csrf.validateRequest(ctx.req, ctx.cb)
expect(ctx.cb.calledWith()).to.equal(true)
})
})
})
describe('validateToken', function () {
describe('when the request is invalid', function () {
it('calls the callback with `false`', function () {
this.cb = sinon.stub()
this.Csrf.validateToken('token', {}, this.cb)
expect(this.cb.calledWith(this.err)).to.equal(true)
it('calls the callback with `false`', function (ctx) {
ctx.cb = sinon.stub()
ctx.Csrf.validateToken('token', {}, ctx.cb)
expect(ctx.cb.calledWith(ctx.err)).to.equal(true)
})
})
describe('when the request is valid', function () {
it('calls the callback with `true`', function () {
this.Csrf = SandboxedModule.require(modulePath, {
globals: {
console,
},
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()).to.equal(true)
it('calls the callback with `true`', async function (ctx) {
vi.doMock('csurf', () => ({
default: (ctx.csurf = sinon
.stub()
.returns((ctx.csurf_csrf = sinon.stub().callsArg(2)))),
}))
ctx.Csrf = (await import(modulePath)).default
ctx.cb = sinon.stub()
ctx.Csrf.validateToken('goodtoken', {}, ctx.cb)
expect(ctx.cb.calledWith()).to.equal(true)
})
})
describe('when there is no token', function () {
it('calls the callback with an error', function () {
this.Csrf = SandboxedModule.require(modulePath, {
globals: {
console,
},
requires: {
csurf: (this.csurf = sinon
.stub()
.returns((this.csurf_csrf = sinon.stub().callsArg(2)))),
},
})
this.cb = sinon.stub()
this.Csrf.validateToken(null, {}, error => {
it('calls the callback with an error', async function (ctx) {
vi.doMock('csurf', () => ({
default: (ctx.csurf = sinon
.stub()
.returns((ctx.csurf_csrf = sinon.stub().callsArg(2)))),
}))
ctx.Csrf = (await import(modulePath)).default
ctx.cb = sinon.stub()
ctx.Csrf.validateToken(null, {}, error => {
expect(error).to.exist
})
})
@@ -1,19 +1,18 @@
const SandboxedModule = require('sandboxed-module')
const assert = require('assert')
const path = require('path')
const sinon = require('sinon')
const { expect } = require('chai')
import { assert, describe, beforeEach, it, vi, expect } from 'vitest'
import path from 'path'
import sinon from 'sinon'
const modulePath = path.join(
__dirname,
import.meta.dirname,
'../../../../app/src/infrastructure/GeoIpLookup'
)
describe('GeoIpLookup', function () {
beforeEach(function () {
this.ipAddress = '12.34.56.78'
beforeEach(async function (ctx) {
ctx.ipAddress = '12.34.56.78'
this.stubbedResponse = {
ip: this.ipAddress,
ctx.stubbedResponse = {
ip: ctx.ipAddress,
country_code: 'GB',
country_name: 'United Kingdom',
region_code: 'H9',
@@ -25,63 +24,67 @@ describe('GeoIpLookup', function () {
metro_code: '',
area_code: '',
}
this.fetchUtils = {
fetchJson: sinon.stub().resolves(this.stubbedResponse),
ctx.fetchUtils = {
fetchJson: sinon.stub().resolves(ctx.stubbedResponse),
}
this.settings = {
ctx.settings = {
apis: {
geoIpLookup: {
url: 'http://lookup.com/',
},
},
}
this.GeoIpLookup = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/fetch-utils': this.fetchUtils,
'@overleaf/settings': this.settings,
},
})
vi.doMock('@overleaf/fetch-utils', () => ({
...ctx.fetchUtils,
}))
vi.doMock('@overleaf/settings', () => ({
default: ctx.settings,
}))
ctx.GeoIpLookup = (await import(modulePath)).default
})
describe('isValidCurrencyParam', function () {
it('should reject invalid currency codes', function () {
expect(this.GeoIpLookup.isValidCurrencyParam('GBP')).to.equal(true)
expect(this.GeoIpLookup.isValidCurrencyParam('USD')).to.equal(true)
expect(this.GeoIpLookup.isValidCurrencyParam('AUD')).to.equal(true)
expect(this.GeoIpLookup.isValidCurrencyParam('EUR')).to.equal(true)
expect(this.GeoIpLookup.isValidCurrencyParam('SGD')).to.equal(true)
expect(this.GeoIpLookup.isValidCurrencyParam('WAT')).to.equal(false)
expect(this.GeoIpLookup.isValidCurrencyParam('NON')).to.equal(false)
expect(this.GeoIpLookup.isValidCurrencyParam('LOL')).to.equal(false)
it('should reject invalid currency codes', function (ctx) {
expect(ctx.GeoIpLookup.isValidCurrencyParam('GBP')).to.equal(true)
expect(ctx.GeoIpLookup.isValidCurrencyParam('USD')).to.equal(true)
expect(ctx.GeoIpLookup.isValidCurrencyParam('AUD')).to.equal(true)
expect(ctx.GeoIpLookup.isValidCurrencyParam('EUR')).to.equal(true)
expect(ctx.GeoIpLookup.isValidCurrencyParam('SGD')).to.equal(true)
expect(ctx.GeoIpLookup.isValidCurrencyParam('WAT')).to.equal(false)
expect(ctx.GeoIpLookup.isValidCurrencyParam('NON')).to.equal(false)
expect(ctx.GeoIpLookup.isValidCurrencyParam('LOL')).to.equal(false)
})
})
describe('getDetails', function () {
beforeEach(function () {
this.fetchUtils.fetchJson.resolves(this.stubbedResponse)
beforeEach(function (ctx) {
ctx.fetchUtils.fetchJson.resolves(ctx.stubbedResponse)
})
describe('async', function () {
it('should request the details using the ip', async function () {
await this.GeoIpLookup.promises.getDetails(this.ipAddress)
this.fetchUtils.fetchJson.should.have.been.calledWith(
new URL(this.settings.apis.geoIpLookup.url + this.ipAddress)
it('should request the details using the ip', async function (ctx) {
await ctx.GeoIpLookup.promises.getDetails(ctx.ipAddress)
ctx.fetchUtils.fetchJson.should.have.been.calledWith(
new URL(ctx.settings.apis.geoIpLookup.url + ctx.ipAddress)
)
})
it('should return the ip details', async function () {
const returnedDetails = await this.GeoIpLookup.promises.getDetails(
this.ipAddress
it('should return the ip details', async function (ctx) {
const returnedDetails = await ctx.GeoIpLookup.promises.getDetails(
ctx.ipAddress
)
assert.deepEqual(returnedDetails, this.stubbedResponse)
assert.deepEqual(returnedDetails, ctx.stubbedResponse)
})
it('should take the first ip in the string', async function () {
await this.GeoIpLookup.promises.getDetails(
` ${this.ipAddress} 123.123.123.123 234.234.234.234`
it('should take the first ip in the string', async function (ctx) {
await ctx.GeoIpLookup.promises.getDetails(
` ${ctx.ipAddress} 123.123.123.123 234.234.234.234`
)
this.fetchUtils.fetchJson.should.have.been.calledWith(
new URL(this.settings.apis.geoIpLookup.url + this.ipAddress)
ctx.fetchUtils.fetchJson.should.have.been.calledWith(
new URL(ctx.settings.apis.geoIpLookup.url + ctx.ipAddress)
)
})
})
@@ -89,57 +92,57 @@ describe('GeoIpLookup', function () {
describe('getCurrencyCode', function () {
describe('async', function () {
it('should return GBP for GB country', async function () {
it('should return GBP for GB country', async function (ctx) {
const { currencyCode, countryCode } =
await this.GeoIpLookup.promises.getCurrencyCode(this.ipAddress)
await ctx.GeoIpLookup.promises.getCurrencyCode(ctx.ipAddress)
currencyCode.should.equal('GBP')
countryCode.should.equal('GB')
})
it('should return GBP for gb country', async function () {
this.stubbedResponse.country_code = 'gb'
it('should return GBP for gb country', async function (ctx) {
ctx.stubbedResponse.country_code = 'gb'
const { currencyCode, countryCode } =
await this.GeoIpLookup.promises.getCurrencyCode(this.ipAddress)
await ctx.GeoIpLookup.promises.getCurrencyCode(ctx.ipAddress)
currencyCode.should.equal('GBP')
countryCode.should.equal('GB')
})
it('should return USD for US', async function () {
this.stubbedResponse.country_code = 'US'
it('should return USD for US', async function (ctx) {
ctx.stubbedResponse.country_code = 'US'
const { currencyCode, countryCode } =
await this.GeoIpLookup.promises.getCurrencyCode(this.ipAddress)
await ctx.GeoIpLookup.promises.getCurrencyCode(ctx.ipAddress)
currencyCode.should.equal('USD')
countryCode.should.equal('US')
})
it('should return EUR for DE', async function () {
this.stubbedResponse.country_code = 'DE'
it('should return EUR for DE', async function (ctx) {
ctx.stubbedResponse.country_code = 'DE'
const { currencyCode, countryCode } =
await this.GeoIpLookup.promises.getCurrencyCode(this.ipAddress)
await ctx.GeoIpLookup.promises.getCurrencyCode(ctx.ipAddress)
currencyCode.should.equal('EUR')
countryCode.should.equal('DE')
})
it('should default to USD if there is an error', async function () {
this.fetchUtils.fetchJson.rejects(new Error('foo'))
it('should default to USD if there is an error', async function (ctx) {
ctx.fetchUtils.fetchJson.rejects(new Error('foo'))
const { currencyCode, countryCode } =
await this.GeoIpLookup.promises.getCurrencyCode(this.ipAddress)
await ctx.GeoIpLookup.promises.getCurrencyCode(ctx.ipAddress)
currencyCode.should.equal('USD')
expect(countryCode).to.be.undefined
})
it('should default to USD if there are no details', async function () {
this.fetchUtils.fetchJson.resolves({})
it('should default to USD if there are no details', async function (ctx) {
ctx.fetchUtils.fetchJson.resolves({})
const { currencyCode, countryCode } =
await this.GeoIpLookup.promises.getCurrencyCode(this.ipAddress)
await ctx.GeoIpLookup.promises.getCurrencyCode(ctx.ipAddress)
currencyCode.should.equal('USD')
expect(countryCode).to.be.undefined
})
it('should default to USD if there is no match for their country', async function () {
this.stubbedResponse.country_code = 'Non existant'
it('should default to USD if there is no match for their country', async function (ctx) {
ctx.stubbedResponse.country_code = 'Non existant'
const { currencyCode, countryCode } =
await this.GeoIpLookup.promises.getCurrencyCode(this.ipAddress)
await ctx.GeoIpLookup.promises.getCurrencyCode(ctx.ipAddress)
currencyCode.should.equal('USD')
countryCode.should.equal('NON EXISTANT')
})
@@ -1,31 +1,38 @@
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
import { describe, expect, it, vi } from 'vitest'
const MODULE_PATH = '../../../../app/src/infrastructure/Translations.js'
const MODULE_PATH = '../../../../app/src/infrastructure/Translations.mjs'
describe('Translations', function () {
let req, res, translations
function runMiddlewares(cb) {
translations.i18nMiddleware(req, res, () => {
translations.setLangBasedOnDomainMiddleware(req, res, cb)
})
async function runMiddlewares(cb) {
return await new Promise((resolve, reject) =>
translations.i18nMiddleware(req, res, () => {
translations.setLangBasedOnDomainMiddleware(req, res, (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
})
)
}
beforeEach(function () {
translations = SandboxedModule.require(MODULE_PATH, {
requires: {
'@overleaf/settings': {
i18n: {
escapeHTMLInVars: false,
subdomainLang: {
www: { lngCode: 'en', url: 'https://www.overleaf.com' },
fr: { lngCode: 'fr', url: 'https://fr.overleaf.com' },
da: { lngCode: 'da', url: 'https://da.overleaf.com' },
},
beforeEach(async function () {
vi.doMock('@overleaf/settings', () => ({
default: {
i18n: {
escapeHTMLInVars: false,
subdomainLang: {
www: { lngCode: 'en', url: 'https://www.overleaf.com' },
fr: { lngCode: 'fr', url: 'https://fr.overleaf.com' },
da: { lngCode: 'da', url: 'https://da.overleaf.com' },
},
},
},
})
}))
translations = (await import(MODULE_PATH)).default
req = {
url: '/',
@@ -41,8 +48,8 @@ describe('Translations', function () {
})
describe('translate', function () {
beforeEach(function (done) {
runMiddlewares(done)
beforeEach(async function () {
await runMiddlewares()
})
it('works', function () {
@@ -55,8 +62,8 @@ describe('Translations', function () {
})
describe('interpolation', function () {
beforeEach(function (done) {
runMiddlewares(done)
beforeEach(async function () {
await runMiddlewares()
})
it('works', function () {
@@ -94,34 +101,28 @@ describe('Translations', function () {
})
describe('setLangBasedOnDomainMiddleware', function () {
it('should set the lang to french if the domain is fr', function (done) {
it('should set the lang to french if the domain is fr', async function () {
req.headers.host = 'fr.overleaf.com'
runMiddlewares(() => {
expect(req.lng).to.equal('fr')
done()
})
await runMiddlewares()
expect(req.lng).to.equal('fr')
})
describe('suggestedLanguageSubdomainConfig', function () {
it('should set suggestedLanguageSubdomainConfig if the detected lang is different to subdomain lang', function (done) {
it('should set suggestedLanguageSubdomainConfig if the detected lang is different to subdomain lang', async function () {
req.headers['accept-language'] = 'da, en-gb;q=0.8, en;q=0.7'
req.headers.host = 'fr.overleaf.com'
runMiddlewares(() => {
expect(res.locals.suggestedLanguageSubdomainConfig).to.exist
expect(res.locals.suggestedLanguageSubdomainConfig.lngCode).to.equal(
'da'
)
done()
})
await runMiddlewares()
expect(res.locals.suggestedLanguageSubdomainConfig).to.exist
expect(res.locals.suggestedLanguageSubdomainConfig.lngCode).to.equal(
'da'
)
})
it('should not set suggestedLanguageSubdomainConfig if the detected lang is the same as subdomain lang', function (done) {
it('should not set suggestedLanguageSubdomainConfig if the detected lang is the same as subdomain lang', async function () {
req.headers['accept-language'] = 'da, en-gb;q=0.8, en;q=0.7'
req.headers.host = 'da.overleaf.com'
runMiddlewares(() => {
expect(res.locals.suggestedLanguageSubdomainConfig).to.not.exist
done()
})
await runMiddlewares()
expect(res.locals.suggestedLanguageSubdomainConfig).to.not.exist
})
})
})