Compare commits

..
37 Commits
Author SHA1 Message Date
yu-i-i 3503a7d887 Fixes only for v5.4.1-ext-v3 2025-05-22 21:26:27 +02:00
yu-i-i 77b37b7cf8 Update README.md: Changes to Template Gallery and Sandboxed Compiles, v5.4.1-ext-v3 2025-05-22 21:26:27 +02:00
yu-i-i 047268b79d Template Gallery: replace markdown-it with marked 2025-05-22 04:12:38 +02:00
yu-i-i 527f60316a Refactor Template Gallery; resolves #38 and #39
- Replace free-text license input with a select box
- Improve visual presentation of modals and enhance keyboard interaction
2025-05-14 15:57:27 +02:00
yu-i-i b6eafbcc0a Refactor Sandboxed Compiles 2025-05-14 15:56:06 +02:00
yu-i-i ddd2789933 README.md: v5.4.0-v2 2025-05-05 11:20:48 +02:00
yu-i-i 7e91256ae3 Symbol Palette: improve keyboard input experience 2025-05-05 11:20:48 +02:00
yu-i-i ea2c644695 Symbol Palette: get rid of @reach/tabs 2025-05-05 11:20:48 +02:00
yu-i-i 281174eee9 Add 'poll' to clsi seccomp profile, fixes minted. Thanks, David. 2025-05-05 11:20:48 +02:00
yu-i-i de65f2b785 Update README.md: Template Gallery 2025-05-05 11:20:48 +02:00
yu-i-i 9d9fbf06c4 Make Template Gallery optional; rename environment variables 2025-05-05 11:20:47 +02:00
yu-i-i 39ae15bda0 Add Template Gallery support 2025-05-05 11:19:40 +02:00
yu-i-i f9d7148b6c Enable From External URL feature 2025-05-05 04:55:53 +02:00
yu-i-i dbc3bbe9aa Minor changes in README.md 2025-05-05 04:48:17 +02:00
yu-i-i 6495cb3c76 Track changes / comments: update backend to support frontend changes 2025-05-05 04:48:17 +02:00
yu-i-i 76e9829b73 Allow EXTERNAL_AUTH to be undefined, fixes #26 2025-05-05 04:48:17 +02:00
yu-i-i 8ef6ad5c8e Clarify OIDC redirect URI in README.md (closes #28) 2025-05-05 04:48:17 +02:00
yu-i-i bbcdcce617 Symbol palette: switch to 'OL' UI components and apply minor cosmetic changes 2025-05-05 04:48:16 +02:00
yu-i-i 66b2de0750 Clarify OVERLEAF_OIDC_USER_ID_FIELD parameter usage (closes #24) 2025-05-05 04:48:16 +02:00
yu-i-i 7615dbef54 Make OVERLEAF_OIDC_USER_ID_FIELD support 'email' as a value 2025-05-05 04:48:16 +02:00
yu-i-i 0ff64f51b5 See upstream commit 42ee56e 2025-05-05 04:48:16 +02:00
yu-i-i 4a4957651f Fix glitches in symbol palette after switching to Bootstrap 5 2025-05-05 04:48:16 +02:00
yu-i-i 3414417298 Whitelist /oidc/login endpoint, fixes #21 2025-05-05 04:48:16 +02:00
yu-i-i cafe99d116 Update README.md (add ENV variables to control SAML signature validation) 2025-05-05 04:48:16 +02:00
yu-i-i 3a609993be Add ENV variables to control SAML signature validation 2025-05-05 04:48:16 +02:00
yu-i-i bcf43a4582 Update README.md 2025-05-05 04:48:16 +02:00
yu-i-i 1ee25c02b1 Re-export doLogout (was removed from exports in commit b9fb636). 2025-05-05 04:48:15 +02:00
yu-i-i 0e3af1c01b Refactor authentication code; add OIDC support 2025-05-05 04:48:15 +02:00
yu-i-i cd8e28b462 Allow adding extra flags to LaTeX compiler through environment variable 2025-05-05 04:48:15 +02:00
yu-i-i 035f7c894c Update README.md 2025-05-05 04:48:15 +02:00
yu-i-i 173bcce718 Enable LDAP and SAML authentication support 2025-05-05 04:48:15 +02:00
yu-i-i e56d52905b Enable Symbol Palette 2025-05-05 04:48:15 +02:00
yu-i-i d19eb59548 Allow selecting a TeX Live image for a project 2025-05-05 04:48:14 +02:00
Sam Van den Vonder 7513da4518 Enable Sandboxed Compiles feature 2025-05-05 04:48:14 +02:00
yu-i-i 8c1a06eb94 Enable autocomplete of reference keys feature 2025-05-05 04:48:14 +02:00
yu-i-i c345c0c940 Enable track changes and comments feature 2025-05-05 04:48:14 +02:00
yu-i-i c0651bc13d Redirect non-existing links to Overleaf page 2025-05-05 04:48:14 +02:00
1588 changed files with 37563 additions and 42413 deletions
+1009 -16
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -77,6 +77,7 @@ each service:
| `filestore` | 9235 |
| `notifications` | 9236 |
| `real-time` | 9237 |
| `references` | 9238 |
| `history-v1` | 9239 |
| `project-history` | 9240 |
+1
View File
@@ -13,6 +13,7 @@ NOTIFICATIONS_HOST=notifications
PROJECT_HISTORY_HOST=project-history
REALTIME_HOST=real-time
REDIS_HOST=redis
REFERENCES_HOST=references
SESSION_SECRET=foo
WEBPACK_HOST=webpack
WEB_API_PASSWORD=overleaf
+11
View File
@@ -112,6 +112,17 @@ services:
- ../services/real-time/app.js:/overleaf/services/real-time/app.js
- ../services/real-time/config:/overleaf/services/real-time/config
references:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9238:9229"
volumes:
- ../services/references/app:/overleaf/services/references/app
- ../services/references/config:/overleaf/services/references/config
- ../services/references/app.js:/overleaf/services/references/app.js
web:
command: ["node", "--watch", "app.js", "--watch-locales"]
environment:
+12 -4
View File
@@ -1,5 +1,6 @@
volumes:
clsi-cache:
clsi-output:
filestore-public-files:
filestore-template-files:
filestore-uploads:
@@ -28,13 +29,12 @@ services:
- DOCKER_RUNNER=true
- TEXLIVE_IMAGE=texlive-full # docker build texlive -t texlive-full
- COMPILES_HOST_DIR=${PWD}/compiles
- OUTPUT_HOST_DIR=${PWD}/output
user: root
volumes:
- ${PWD}/compiles:/overleaf/services/clsi/compiles
- ${PWD}/output:/overleaf/services/clsi/output
- ${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock
- clsi-cache:/overleaf/services/clsi/cache
- clsi-output:/overleaf/services/clsi/output
contacts:
build:
@@ -123,7 +123,7 @@ services:
dockerfile: services/real-time/Dockerfile
env_file:
- dev.env
redis:
image: redis:5
ports:
@@ -131,6 +131,13 @@ services:
volumes:
- redis-data:/data
references:
build:
context: ..
dockerfile: services/references/Dockerfile
env_file:
- dev.env
web:
build:
context: ..
@@ -140,7 +147,7 @@ services:
- dev.env
environment:
- APP_NAME=Overleaf Community Edition
- ENABLED_LINKED_FILE_TYPES=project_file,project_output_file
- ENABLED_LINKED_FILE_TYPES=project_file,project_output_file,url
- EMAIL_CONFIRMATION_DISABLED=true
- NODE_ENV=development
- OVERLEAF_ALLOW_PUBLIC_ACCESS=true
@@ -161,6 +168,7 @@ services:
- notifications
- project-history
- real-time
- references
webpack:
build:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+4 -12
View File
@@ -32,7 +32,7 @@ services:
OVERLEAF_REDIS_HOST: redis
REDIS_HOST: redis
ENABLED_LINKED_FILE_TYPES: 'project_file,project_output_file'
ENABLED_LINKED_FILE_TYPES: 'project_file,project_output_file,url'
# Enables Thumbnail generation using ImageMagick
ENABLE_CONVERSIONS: 'true'
@@ -73,19 +73,11 @@ services:
## Server Pro ##
################
## The Community Edition is intended for use in environments where all users are trusted and is not appropriate for
## scenarios where isolation of users is required. Sandboxed Compiles are not available in the Community Edition,
## so the following environment variables must be commented out to avoid compile issues.
##
## Sandboxed Compiles: https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles
## Sandboxed Compiles: https://github.com/overleaf/overleaf/wiki/Server-Pro:-Sandboxed-Compiles
SANDBOXED_COMPILES: 'true'
### Bind-mount source for /var/lib/overleaf/data/compiles inside the container.
SANDBOXED_COMPILES_HOST_DIR_COMPILES: '/home/user/sharelatex_data/data/compiles'
### Bind-mount source for /var/lib/overleaf/data/output inside the container.
SANDBOXED_COMPILES_HOST_DIR_OUTPUT: '/home/user/sharelatex_data/data/output'
### Backwards compatibility (before Server Pro 5.5)
DOCKER_RUNNER: 'true'
SANDBOXED_COMPILES_SIBLING_CONTAINERS: 'true'
### Bind-mount source for /var/lib/overleaf/data/compiles inside the container.
SANDBOXED_COMPILES_HOST_DIR: '/home/user/sharelatex_data/data/compiles'
## Works with test LDAP server shown at bottom of docker compose
# OVERLEAF_LDAP_URL: 'ldap://ldap:389'
@@ -0,0 +1 @@
node_modules/
@@ -0,0 +1,46 @@
compileFolder
Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db
/node_modules/*
data/*/*
**.swp
/log.json
hash_folder
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
@@ -5,6 +5,6 @@ access-token-encryptor
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
# managed by monorepo$ bin/update_build_scripts
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ fetch-utils
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+13 -19
View File
@@ -23,11 +23,11 @@ async function fetchJson(url, opts = {}) {
}
async function fetchJsonWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts } = parseOpts(opts)
fetchOpts.headers = fetchOpts.headers ?? {}
fetchOpts.headers.Accept = fetchOpts.headers.Accept ?? 'application/json'
const response = await performRequest(url, fetchOpts, detachSignal)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -53,8 +53,8 @@ async function fetchStream(url, opts = {}) {
}
async function fetchStreamWithResponse(url, opts = {}) {
const { fetchOpts, abortController, detachSignal } = parseOpts(opts)
const response = await performRequest(url, fetchOpts, detachSignal)
const { fetchOpts, abortController } = parseOpts(opts)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
@@ -76,8 +76,8 @@ async function fetchStreamWithResponse(url, opts = {}) {
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchNothing(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const response = await performRequest(url, fetchOpts, detachSignal)
const { fetchOpts } = parseOpts(opts)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -108,9 +108,9 @@ async function fetchRedirect(url, opts = {}) {
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirectWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const { fetchOpts } = parseOpts(opts)
fetchOpts.redirect = 'manual'
const response = await performRequest(url, fetchOpts, detachSignal)
const response = await performRequest(url, fetchOpts)
if (response.status < 300 || response.status >= 400) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -142,8 +142,8 @@ async function fetchString(url, opts = {}) {
}
async function fetchStringWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts)
const response = await performRequest(url, fetchOpts, detachSignal)
const { fetchOpts } = parseOpts(opts)
const response = await performRequest(url, fetchOpts)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
@@ -178,14 +178,13 @@ function parseOpts(opts) {
const abortController = new AbortController()
fetchOpts.signal = abortController.signal
let detachSignal = () => {}
if (opts.signal) {
detachSignal = abortOnSignal(abortController, opts.signal)
abortOnSignal(abortController, opts.signal)
}
if (opts.body instanceof Readable) {
abortOnDestroyedRequest(abortController, fetchOpts.body)
}
return { fetchOpts, abortController, detachSignal }
return { fetchOpts, abortController }
}
function setupJsonBody(fetchOpts, json) {
@@ -209,9 +208,6 @@ function abortOnSignal(abortController, signal) {
abortController.abort(signal.reason)
}
signal.addEventListener('abort', listener)
return () => {
signal.removeEventListener('abort', listener)
}
}
function abortOnDestroyedRequest(abortController, stream) {
@@ -230,12 +226,11 @@ function abortOnDestroyedResponse(abortController, response) {
})
}
async function performRequest(url, fetchOpts, detachSignal) {
async function performRequest(url, fetchOpts) {
let response
try {
response = await fetch(url, fetchOpts)
} catch (err) {
detachSignal()
if (fetchOpts.body instanceof Readable) {
fetchOpts.body.destroy()
}
@@ -244,7 +239,6 @@ async function performRequest(url, fetchOpts, detachSignal) {
method: fetchOpts.method ?? 'GET',
})
}
response.body.on('close', detachSignal)
if (fetchOpts.body instanceof Readable) {
response.body.on('close', () => {
if (!fetchOpts.body.readableEnded) {
@@ -1,9 +1,6 @@
const { expect } = require('chai')
const fs = require('node:fs')
const events = require('node:events')
const { FetchError, AbortError } = require('node-fetch')
const { Readable } = require('node:stream')
const { pipeline } = require('node:stream/promises')
const { once } = require('node:events')
const { TestServer } = require('./helpers/TestServer')
const selfsigned = require('selfsigned')
@@ -206,31 +203,6 @@ describe('fetch-utils', function () {
).to.be.rejectedWith(AbortError)
expect(stream.destroyed).to.be.true
})
it('detaches from signal on success', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
const s = await fetchStream(this.url('/hello'), { signal })
expect(events.getEventListeners(signal, 'abort')).to.have.length(1)
await pipeline(s, fs.createWriteStream('/dev/null'))
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
})
it('detaches from signal on error', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
try {
await fetchStream(this.url('/500'), { signal })
} catch (err) {
if (err instanceof RequestFailedError && err.response.status === 500)
continue
throw err
} finally {
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
}
})
})
describe('fetchNothing', function () {
@@ -419,16 +391,9 @@ async function* infiniteIterator() {
async function abortOnceReceived(func, server) {
const controller = new AbortController()
const promise = func(controller.signal)
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(1)
await once(server.events, 'request-received')
controller.abort()
try {
return await promise
} finally {
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(
0
)
}
return await promise
}
async function expectRequestAborted(req) {
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
node_modules
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ logger
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
node_modules
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ metrics
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
-4
View File
@@ -5,8 +5,6 @@
* before any other module to support code instrumentation.
*/
const metricsModuleImportStartTime = performance.now()
const APP_NAME = process.env.METRICS_APP_NAME || 'unknown'
const BUILD_VERSION = process.env.BUILD_VERSION
const ENABLE_PROFILE_AGENT = process.env.ENABLE_PROFILE_AGENT === 'true'
@@ -105,5 +103,3 @@ function recordProcessStart() {
const metrics = require('.')
metrics.inc('process_startup')
}
module.exports = { metricsModuleImportStartTime }
+1 -1
View File
@@ -9,7 +9,7 @@
"main": "index.js",
"dependencies": {
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0",
"@google-cloud/profiler": "^6.0.3",
"@google-cloud/profiler": "^6.0.0",
"@opentelemetry/api": "^1.4.1",
"@opentelemetry/auto-instrumentations-node": "^0.39.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.41.2",
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
# managed by monorepo$ bin/update_build_scripts
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+43 -57
View File
@@ -16,7 +16,6 @@ let VERBOSE_LOGGING
let BATCH_RANGE_START
let BATCH_RANGE_END
let BATCH_MAX_TIME_SPAN_IN_MS
let BATCHED_UPDATE_RUNNING = false
/**
* @typedef {import("mongodb").Collection} Collection
@@ -35,7 +34,6 @@ let BATCHED_UPDATE_RUNNING = false
* @property {string} [BATCH_RANGE_START]
* @property {string} [BATCH_SIZE]
* @property {string} [VERBOSE_LOGGING]
* @property {(progress: string) => Promise<void>} [trackProgress]
*/
/**
@@ -211,71 +209,59 @@ async function batchedUpdate(
update,
projection,
findOptions,
batchedUpdateOptions = {}
batchedUpdateOptions
) {
// only a single batchedUpdate can run at a time due to global variables
if (BATCHED_UPDATE_RUNNING) {
throw new Error('batchedUpdate is already running')
ID_EDGE_PAST = await getIdEdgePast(collection)
if (!ID_EDGE_PAST) {
console.warn(
`The collection ${collection.collectionName} appears to be empty.`
)
return 0
}
try {
BATCHED_UPDATE_RUNNING = true
ID_EDGE_PAST = await getIdEdgePast(collection)
if (!ID_EDGE_PAST) {
console.warn(
`The collection ${collection.collectionName} appears to be empty.`
)
return 0
}
refreshGlobalOptionsForBatchedUpdate(batchedUpdateOptions)
const { trackProgress = async progress => console.warn(progress) } =
batchedUpdateOptions
refreshGlobalOptionsForBatchedUpdate(batchedUpdateOptions)
findOptions = findOptions || {}
findOptions.readPreference = READ_PREFERENCE_SECONDARY
findOptions = findOptions || {}
findOptions.readPreference = READ_PREFERENCE_SECONDARY
projection = projection || { _id: 1 }
let nextBatch
let updated = 0
let start = BATCH_RANGE_START
projection = projection || { _id: 1 }
let nextBatch
let updated = 0
let start = BATCH_RANGE_START
while (start !== BATCH_RANGE_END) {
let end = getNextEnd(start)
nextBatch = await getNextBatch(
collection,
query,
start,
end,
projection,
findOptions
)
if (nextBatch.length > 0) {
end = nextBatch[nextBatch.length - 1]._id
updated += nextBatch.length
while (start !== BATCH_RANGE_END) {
let end = getNextEnd(start)
nextBatch = await getNextBatch(
collection,
query,
start,
end,
projection,
findOptions
)
if (nextBatch.length > 0) {
end = nextBatch[nextBatch.length - 1]._id
updated += nextBatch.length
if (VERBOSE_LOGGING) {
console.log(
`Running update on batch with ids ${JSON.stringify(
nextBatch.map(entry => entry._id)
)}`
)
}
await trackProgress(
`Running update on batch ending ${renderObjectId(end)}`
if (VERBOSE_LOGGING) {
console.log(
`Running update on batch with ids ${JSON.stringify(
nextBatch.map(entry => entry._id)
)}`
)
if (typeof update === 'function') {
await update(nextBatch)
} else {
await performUpdate(collection, nextBatch, update)
}
} else {
console.error(`Running update on batch ending ${renderObjectId(end)}`)
}
if (typeof update === 'function') {
await update(nextBatch)
} else {
await performUpdate(collection, nextBatch, update)
}
await trackProgress(`Completed batch ending ${renderObjectId(end)}`)
start = end
}
return updated
} finally {
BATCHED_UPDATE_RUNNING = false
console.error(`Completed batch ending ${renderObjectId(end)}`)
start = end
}
return updated
}
/**
+2 -2
View File
@@ -5,6 +5,6 @@ mongo-utils
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+1
View File
@@ -0,0 +1 @@
node_modules/
+5
View File
@@ -0,0 +1,5 @@
.nyc_output
coverage
node_modules/
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ o-error
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+10 -27
View File
@@ -1,34 +1,20 @@
// @ts-check
/**
* Light-weight helpers for handling JavaScript Errors in node.js and the
* browser.
*/
class OError extends Error {
/**
* The error that is the underlying cause of this error
*
* @type {unknown}
*/
cause
/**
* List of errors encountered as the callback chain is unwound
*
* @type {TaggedError[] | undefined}
*/
_oErrorTags
/**
* @param {string} message as for built-in Error
* @param {Object} [info] extra data to attach to the error
* @param {unknown} [cause] the internal error that caused this error
* @param {Error} [cause] the internal error that caused this error
*/
constructor(message, info, cause) {
super(message)
this.name = this.constructor.name
if (info) this.info = info
if (cause) this.cause = cause
/** @private @type {Array<TaggedError> | undefined} */
this._oErrorTags // eslint-disable-line
}
/**
@@ -45,7 +31,7 @@ class OError extends Error {
/**
* Wrap the given error, which caused this error.
*
* @param {unknown} cause the internal error that caused this error
* @param {Error} cause the internal error that caused this error
* @return {this}
*/
withCause(cause) {
@@ -79,16 +65,13 @@ class OError extends Error {
* }
* }
*
* @template {unknown} E
* @param {E} error the error to tag
* @param {Error} error the error to tag
* @param {string} [message] message with which to tag `error`
* @param {Object} [info] extra data with wich to tag `error`
* @return {E} the modified `error` argument
* @return {Error} the modified `error` argument
*/
static tag(error, message, info) {
const oError = /** @type {{ _oErrorTags: TaggedError[] | undefined }} */ (
error
)
const oError = /** @type{OError} */ (error)
if (!oError._oErrorTags) oError._oErrorTags = []
@@ -119,7 +102,7 @@ class OError extends Error {
*
* If an info property is repeated, the last one wins.
*
* @param {unknown} error any error (may or may not be an `OError`)
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
* @return {Object}
*/
static getFullInfo(error) {
@@ -146,7 +129,7 @@ class OError extends Error {
* Return the `stack` property from `error`, including the `stack`s for any
* tagged errors added with `OError.tag` and for any `cause`s.
*
* @param {unknown} error any error (may or may not be an `OError`)
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
* @return {string}
*/
static getFullStack(error) {
@@ -160,7 +143,7 @@ class OError extends Error {
stack += `\n${oError._oErrorTags.map(tag => tag.stack).join('\n')}`
}
const causeStack = OError.getFullStack(oError.cause)
const causeStack = oError.cause && OError.getFullStack(oError.cause)
if (causeStack) {
stack += '\ncaused by:\n' + indent(causeStack)
}
@@ -268,11 +268,6 @@ describe('utils', function () {
expect(OError.getFullInfo(null)).to.deep.equal({})
})
it('works when given a string', function () {
const err = 'not an error instance'
expect(OError.getFullInfo(err)).to.deep.equal({})
})
it('works on a normal error', function () {
const err = new Error('foo')
expect(OError.getFullInfo(err)).to.deep.equal({})
-8
View File
@@ -35,14 +35,6 @@ describe('OError', function () {
expect(err2.cause.message).to.equal('cause 2')
})
it('accepts non-Error causes', function () {
const err1 = new OError('foo', {}, 'not-an-error')
expect(err1.cause).to.equal('not-an-error')
const err2 = new OError('foo').withCause('not-an-error')
expect(err2.cause).to.equal('not-an-error')
})
it('handles a custom error type with a cause', function () {
function doSomethingBadInternally() {
throw new Error('internal error')
+1
View File
@@ -0,0 +1 @@
node_modules/
+4
View File
@@ -0,0 +1,4 @@
/node_modules
*.swp
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ object-persistor
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
@@ -305,10 +305,8 @@ module.exports = class FSPersistor extends AbstractPersistor {
async _listDirectory(path) {
if (this.useSubdirectories) {
// eslint-disable-next-line @typescript-eslint/return-await
return await glob(Path.join(path, '**'))
} else {
// eslint-disable-next-line @typescript-eslint/return-await
return await glob(`${path}_*`)
}
}
@@ -0,0 +1 @@
node_modules/
@@ -0,0 +1,5 @@
/coverage
/node_modules
# managed by monorepo$ bin/update_build_scripts
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
@@ -5,6 +5,6 @@ overleaf-editor-core
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
-6
View File
@@ -18,7 +18,6 @@ const MoveFileOperation = require('./lib/operation/move_file_operation')
const SetCommentStateOperation = require('./lib/operation/set_comment_state_operation')
const EditFileOperation = require('./lib/operation/edit_file_operation')
const EditNoOperation = require('./lib/operation/edit_no_operation')
const EditOperationTransformer = require('./lib/operation/edit_operation_transformer')
const SetFileMetadataOperation = require('./lib/operation/set_file_metadata_operation')
const NoOperation = require('./lib/operation/no_operation')
const Operation = require('./lib/operation')
@@ -44,8 +43,6 @@ const TrackingProps = require('./lib/file_data/tracking_props')
const Range = require('./lib/range')
const CommentList = require('./lib/file_data/comment_list')
const LazyStringFileData = require('./lib/file_data/lazy_string_file_data')
const StringFileData = require('./lib/file_data/string_file_data')
const EditOperationBuilder = require('./lib/operation/edit_operation_builder')
exports.AddCommentOperation = AddCommentOperation
exports.Author = Author
@@ -61,7 +58,6 @@ exports.DeleteCommentOperation = DeleteCommentOperation
exports.File = File
exports.FileMap = FileMap
exports.LazyStringFileData = LazyStringFileData
exports.StringFileData = StringFileData
exports.History = History
exports.Label = Label
exports.AddFileOperation = AddFileOperation
@@ -69,8 +65,6 @@ exports.MoveFileOperation = MoveFileOperation
exports.SetCommentStateOperation = SetCommentStateOperation
exports.EditFileOperation = EditFileOperation
exports.EditNoOperation = EditNoOperation
exports.EditOperationBuilder = EditOperationBuilder
exports.EditOperationTransformer = EditOperationTransformer
exports.SetFileMetadataOperation = SetFileMetadataOperation
exports.NoOperation = NoOperation
exports.Operation = Operation
+2 -2
View File
@@ -13,7 +13,7 @@ const V2DocVersions = require('./v2_doc_versions')
/**
* @import Author from "./author"
* @import { BlobStore, RawChange } from "./types"
* @import { BlobStore } from "./types"
*/
/**
@@ -54,7 +54,7 @@ class Change {
/**
* For serialization.
*
* @return {RawChange}
* @return {Object}
*/
toRaw() {
function toRaw(object) {
@@ -88,14 +88,6 @@ class StringFileData extends FileData {
return content
}
/**
* Return docstore view of a doc: each line separated
* @return {string[]}
*/
getLines() {
return this.getContent({ filterTrackedDeletes: true }).split('\n')
}
/** @inheritdoc */
getByteLength() {
return Buffer.byteLength(this.content)
@@ -36,20 +36,6 @@ class EditOperationBuilder {
}
throw new Error('Unsupported operation in EditOperationBuilder.fromJSON')
}
/**
* @param {unknown} raw
* @return {raw is RawEditOperation}
*/
static isValid(raw) {
return (
isTextOperation(raw) ||
isRawAddCommentOperation(raw) ||
isRawDeleteCommentOperation(raw) ||
isRawSetCommentStateOperation(raw) ||
isRawEditNoOperation(raw)
)
}
}
/**
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
# managed by monorepo$ bin/update_build_scripts
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ promise-utils
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+1
View File
@@ -0,0 +1 @@
node_modules/
+13
View File
@@ -0,0 +1,13 @@
**.swp
app.js
app/js/
test/unit/js/
public/build/
node_modules/
/public/js/chat.js
plato/
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ ranges-tracker
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+1
View File
@@ -0,0 +1 @@
node_modules/
+13
View File
@@ -0,0 +1,13 @@
**.swp
app.js
app/js/
test/unit/js/
public/build/
node_modules/
/public/js/chat.js
plato/
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+28 -27
View File
@@ -97,8 +97,7 @@ module.exports = class RedisLocker {
}
/**
* @param {string} id
* @param {function(Error, boolean, string): void} callback
* @param {Callback} callback
*/
tryLock(id, callback) {
if (callback == null) {
@@ -107,7 +106,7 @@ module.exports = class RedisLocker {
const lockValue = this.randomLock()
const key = this.getKey(id)
const startTime = Date.now()
this.rclient.set(
return this.rclient.set(
key,
lockValue,
'EX',
@@ -122,7 +121,7 @@ module.exports = class RedisLocker {
const timeTaken = Date.now() - startTime
if (timeTaken > MAX_REDIS_REQUEST_LENGTH) {
// took too long, so try to free the lock
this.releaseLock(id, lockValue, function (err, result) {
return this.releaseLock(id, lockValue, function (err, result) {
if (err != null) {
return callback(err)
} // error freeing lock
@@ -140,8 +139,7 @@ module.exports = class RedisLocker {
}
/**
* @param {string} id
* @param {function(Error, string): void} callback
* @param {Callback} callback
*/
getLock(id, callback) {
if (callback == null) {
@@ -155,7 +153,7 @@ module.exports = class RedisLocker {
return callback(e)
}
this.tryLock(id, (error, gotLock, lockValue) => {
return this.tryLock(id, (error, gotLock, lockValue) => {
if (error != null) {
return callback(error)
}
@@ -175,15 +173,14 @@ module.exports = class RedisLocker {
}
/**
* @param {string} id
* @param {function(Error, boolean): void} callback
* @param {Callback} callback
*/
checkLock(id, callback) {
if (callback == null) {
callback = function () {}
}
const key = this.getKey(id)
this.rclient.exists(key, (err, exists) => {
return this.rclient.exists(key, (err, exists) => {
if (err != null) {
return callback(err)
}
@@ -199,26 +196,30 @@ module.exports = class RedisLocker {
}
/**
* @param {string} id
* @param {string} lockValue
* @param {function(Error, boolean): void} callback
* @param {Callback} callback
*/
releaseLock(id, lockValue, callback) {
const key = this.getKey(id)
this.rclient.eval(UNLOCK_SCRIPT, 1, key, lockValue, (err, result) => {
if (err != null) {
return callback(err)
} else if (result != null && result !== 1) {
// successful unlock should release exactly one key
logger.error(
{ id, key, lockValue, redis_err: err, redis_result: result },
'unlocking error'
)
metrics.inc(this.metricsPrefix + '-unlock-error')
return callback(new Error('tried to release timed out lock'))
} else {
return callback(null, result)
return this.rclient.eval(
UNLOCK_SCRIPT,
1,
key,
lockValue,
(err, result) => {
if (err != null) {
return callback(err)
} else if (result != null && result !== 1) {
// successful unlock should release exactly one key
logger.error(
{ id, key, lockValue, redis_err: err, redis_result: result },
'unlocking error'
)
metrics.inc(this.metricsPrefix + '-unlock-error')
return callback(new Error('tried to release timed out lock'))
} else {
return callback(null, result)
}
}
})
)
}
}
+2 -2
View File
@@ -5,6 +5,6 @@ redis-wrapper
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+1
View File
@@ -0,0 +1 @@
node_modules/
+5
View File
@@ -0,0 +1,5 @@
/.npmrc
/node_modules
# managed by monorepo$ bin/update_build_scripts
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ settings
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+1
View File
@@ -0,0 +1 @@
node_modules/
+3
View File
@@ -0,0 +1,3 @@
# managed by monorepo$ bin/update_build_scripts
.npmrc
+1 -1
View File
@@ -1 +1 @@
22.15.1
20.18.2
+2 -2
View File
@@ -5,6 +5,6 @@ stream-utils
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=22.15.1
--node-version=20.18.2
--public-repo=False
--script-version=4.7.0
--script-version=4.5.0
+3445 -3596
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -37,7 +37,7 @@
},
"swagger-tools": {
"body-parser": "1.20.3",
"multer": "2.0.0",
"multer": "1.4.5-lts.1",
"path-to-regexp": "3.3.0",
"qs": "6.13.0"
}
@@ -55,7 +55,6 @@
"services/analytics",
"services/chat",
"services/clsi",
"services/clsi-cache",
"services/clsi-perf",
"services/contacts",
"services/docstore",
+23
View File
@@ -0,0 +1,23 @@
diff --git a/node_modules/@node-saml/node-saml/lib/saml.js b/node_modules/@node-saml/node-saml/lib/saml.js
index fba15b9..a5778cb 100644
--- a/node_modules/@node-saml/node-saml/lib/saml.js
+++ b/node_modules/@node-saml/node-saml/lib/saml.js
@@ -336,7 +336,8 @@ class SAML {
const requestOrResponse = request || response;
(0, utility_1.assertRequired)(requestOrResponse, "either request or response is required");
let buffer;
- if (this.options.skipRequestCompression) {
+ // logout requestOrResponse must be compressed anyway
+ if (this.options.skipRequestCompression && operation !== "logout") {
buffer = Buffer.from(requestOrResponse, "utf8");
}
else {
@@ -495,7 +496,7 @@ class SAML {
try {
xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8");
doc = await (0, xml_1.parseDomFromString)(xml);
- const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
+ const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response' or local-name()='LogoutResponse']/@InResponseTo");
if (inResponseToNodes) {
inResponseTo = inResponseToNodes.length ? inResponseToNodes[0].nodeValue : null;
await this.validateInResponseTo(inResponseTo);
+64
View File
@@ -0,0 +1,64 @@
diff --git a/node_modules/ldapauth-fork/lib/ldapauth.js b/node_modules/ldapauth-fork/lib/ldapauth.js
index 85ecf36a8b..a7d07e0f78 100644
--- a/node_modules/ldapauth-fork/lib/ldapauth.js
+++ b/node_modules/ldapauth-fork/lib/ldapauth.js
@@ -69,6 +69,7 @@ function LdapAuth(opts) {
this.opts.bindProperty || (this.opts.bindProperty = 'dn');
this.opts.groupSearchScope || (this.opts.groupSearchScope = 'sub');
this.opts.groupDnProperty || (this.opts.groupDnProperty = 'dn');
+ this.opts.tlsStarted = false;
EventEmitter.call(this);
@@ -108,21 +109,7 @@ function LdapAuth(opts) {
this._userClient.on('error', this._handleError.bind(this));
var self = this;
- if (this.opts.starttls) {
- // When starttls is enabled, this callback supplants the 'connect' callback
- this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function(err) {
- if (err) {
- self._handleError(err);
- } else {
- self._onConnectAdmin();
- }
- });
- this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function(err) {
- if (err) {
- self._handleError(err);
- }
- });
- } else if (opts.reconnect) {
+ if (opts.reconnect && !this.opts.starttls) {
this.once('_installReconnectListener', function() {
self.log && self.log.trace('install reconnect listener');
self._adminClient.on('connect', function() {
@@ -384,6 +371,28 @@ LdapAuth.prototype._findGroups = function(user, callback) {
*/
LdapAuth.prototype.authenticate = function(username, password, callback) {
var self = this;
+ if (this.opts.starttls && !this.opts.tlsStarted) {
+ // When starttls is enabled, this callback supplants the 'connect' callback
+ this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function (err) {
+ if (err) {
+ self._handleError(err);
+ } else {
+ self._onConnectAdmin(function(){self._handleAuthenticate(username, password, callback);});
+ }
+ });
+ this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function (err) {
+ if (err) {
+ self._handleError(err);
+ }
+ });
+ } else {
+ self._handleAuthenticate(username, password, callback);
+ }
+};
+
+LdapAuth.prototype._handleAuthenticate = function (username, password, callback) {
+ this.opts.tlsStarted = true;
+ var self = this;
if (typeof password === 'undefined' || password === null || password === '') {
return callback(new Error('no password given'));
-22
View File
@@ -1,22 +0,0 @@
diff --git a/node_modules/pdfjs-dist/build/pdf.worker.mjs b/node_modules/pdfjs-dist/build/pdf.worker.mjs
index 6c5c6f1..bb6b7d1 100644
--- a/node_modules/pdfjs-dist/build/pdf.worker.mjs
+++ b/node_modules/pdfjs-dist/build/pdf.worker.mjs
@@ -1830,7 +1830,7 @@ async function __wbg_init(module_or_path) {
}
}
if (typeof module_or_path === 'undefined') {
- module_or_path = new URL('qcms_bg.wasm', import.meta.url);
+ module_or_path = new URL(/* webpackIgnore: true */ 'qcms_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || typeof Request === 'function' && module_or_path instanceof Request || typeof URL === 'function' && module_or_path instanceof URL) {
@@ -5358,7 +5358,7 @@ var OpenJPEG = (() => {
if (Module["locateFile"]) {
return locateFile("openjpeg.wasm");
}
- return new URL("openjpeg.wasm", import.meta.url).href;
+ return new URL(/* webpackIgnore: true */ "openjpeg.wasm", import.meta.url).href;
}
function getBinarySync(file) {
if (file == wasmBinaryFile && wasmBinary) {
+3 -3
View File
@@ -2,7 +2,7 @@
# Overleaf Base Image (sharelatex/sharelatex-base)
# --------------------------------------------------
FROM phusion/baseimage:noble-1.0.2
FROM phusion/baseimage:noble-1.0.0
# Makes sure LuaTex cache is writable
# -----------------------------------
@@ -10,7 +10,7 @@ ENV TEXMFVAR=/var/lib/overleaf/tmp/texmf-var
# Update to ensure dependencies are updated
# ------------------------------------------
ENV REBUILT_AFTER="2025-05-19"
ENV REBUILT_AFTER="2025-03-27"
# Install dependencies
# --------------------
@@ -30,7 +30,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# install Node.js https://github.com/nodesource/distributions#nodejs
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
\
-1
View File
@@ -1,4 +1,3 @@
*/20 * * * * root /overleaf/cron/project-history-periodic-flush.sh >> /var/log/overleaf/cron-project-history-periodic-flush.log 2>&1
30 * * * * root /overleaf/cron/project-history-retry-soft.sh >> /var/log/overleaf/project-history-retry-soft.log 2>&1
45 * * * * root /overleaf/cron/project-history-retry-hard.sh >> /var/log/overleaf/project-history-retry-hard.log 2>&1
0 3 * * * root /overleaf/cron/project-history-flush-all.sh >> /var/log/overleaf/project-history-flush-all.log 2>&1
@@ -45,17 +45,5 @@
"clusterWorkers": "CLUSTER_WORKERS",
"maxFileUploadSize": "MAX_FILE_UPLOAD_SIZE",
"httpsOnly": "HTTPS_ONLY",
"httpRequestTimeout": "OVERLEAF_HISTORY_V1_HTTP_REQUEST_TIMEOUT",
"redis": {
"history": {
"host": "OVERLEAF_REDIS_HOST",
"password": "OVERLEAF_REDIS_PASS",
"port": "OVERLEAF_REDIS_PORT"
},
"lock": {
"host": "OVERLEAF_REDIS_HOST",
"password": "OVERLEAF_REDIS_PASS",
"port": "OVERLEAF_REDIS_PORT"
}
}
"httpRequestTimeout": "OVERLEAF_HISTORY_V1_HTTP_REQUEST_TIMEOUT"
}
+1
View File
@@ -9,5 +9,6 @@ export HISTORY_V1_HOST=127.0.0.1
export NOTIFICATIONS_HOST=127.0.0.1
export PROJECT_HISTORY_HOST=127.0.0.1
export REALTIME_HOST=127.0.0.1
export REFERENCES_HOST=127.0.0.1
export WEB_HOST=127.0.0.1
export WEB_API_HOST=127.0.0.1
-2
View File
@@ -79,7 +79,6 @@ const settings = {
host: process.env.OVERLEAF_REDIS_HOST || 'dockerhost',
port: process.env.OVERLEAF_REDIS_PORT || '6379',
password: process.env.OVERLEAF_REDIS_PASS || undefined,
tls: process.env.OVERLEAF_REDIS_TLS === 'true' ? {} : undefined,
key_schema: {
// document-updater
blockingKey({ doc_id }) {
@@ -140,7 +139,6 @@ const settings = {
api: redisConfig,
pubsub: redisConfig,
project_history: redisConfig,
references: redisConfig,
project_history_migration: {
host: redisConfig.host,
@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -eux
echo "---------------------------------"
echo "Flush all project-history changes"
echo "---------------------------------"
date
source /etc/container_environment.sh
source /etc/overleaf/env.sh
cd /overleaf/services/project-history && node scripts/flush_all.js
echo "Done flushing all project-history changes"
-14
View File
@@ -1,14 +0,0 @@
FROM sharelatex/sharelatex:5.4.0
RUN apt update && apt install -y linux-libc-dev \
&& unattended-upgrade --verbose --no-minimal-upgrade-steps \
&& rm -rf /var/lib/apt/lists/*
COPY package-lock.json.diff .
RUN patch package-lock.json < package-lock.json.diff
RUN npm install --omit=dev
# fix tls configuration in redis
COPY issue_24996.patch .
RUN patch -p0 /etc/overleaf/settings.js < issue_24996.patch \
&& rm issue_24996.patch
-10
View File
@@ -1,10 +0,0 @@
--- settings.js
+++ settings.js
@@ -79,6 +79,7 @@ const settings = {
host: process.env.OVERLEAF_REDIS_HOST || 'dockerhost',
port: process.env.OVERLEAF_REDIS_PORT || '6379',
password: process.env.OVERLEAF_REDIS_PASS || undefined,
+ tls: process.env.OVERLEAF_REDIS_TLS === 'true' ? {} : undefined,
key_schema: {
// document-updater
blockingKey({ doc_id }) {
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
NODE_PARAMS=""
if [ "$DEBUG_NODE" == "true" ]; then
echo "running debug - references"
NODE_PARAMS="--inspect=0.0.0.0:30560"
fi
source /etc/overleaf/env.sh
export LISTEN_ADDRESS=127.0.0.1
exec /sbin/setuser www-data /usr/bin/node $NODE_PARAMS /overleaf/services/references/app.js >> /var/log/overleaf/references.log 2>&1
+3
View File
@@ -29,6 +29,9 @@ module.exports = [
{
name: 'project-history',
},
{
name: 'references',
},
{
name: 'history-v1',
},
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.15.1
FROM node:20.18.2
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - \
&& echo \
"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
+1 -1
View File
@@ -9,7 +9,7 @@ describe('Accounts', function () {
it('can log in and out', function () {
login('user@example.com')
cy.visit('/project')
cy.findByRole('menuitem', { name: 'Account' }).click()
cy.findByText('Account').click()
cy.findByText('Log Out').click()
cy.url().should('include', '/login')
cy.visit('/project')
+4 -4
View File
@@ -293,8 +293,8 @@ describe('admin panel', function () {
cy.findByText(deletedProjectName).should('not.exist')
cy.log('navigate to thrashed projects and delete the project')
cy.get('.project-list-sidebar-scroll').within(() => {
cy.findByText('Trashed projects').click()
cy.get('.project-list-sidebar-react').within(() => {
cy.findByText('Trashed Projects').click()
})
findProjectRow(deletedProjectName).within(() =>
cy.findByRole('button', { name: 'Delete' }).click()
@@ -318,8 +318,8 @@ describe('admin panel', function () {
cy.log('login as the user and verify the project is restored')
login(user1)
cy.visit('/project')
cy.get('.project-list-sidebar-scroll').within(() => {
cy.findByText('Trashed projects').click()
cy.get('.project-list-sidebar-react').within(() => {
cy.findByText('Trashed Projects').click()
})
cy.findByText(`${deletedProjectName} (Restored)`)
})
@@ -51,7 +51,7 @@ describe('Project creation and compilation', function () {
login('user@example.com')
createProject(sourceProjectName, {
type: 'Example project',
type: 'Example Project',
open: false,
}).as('sourceProjectId')
createProject(targetProjectName)
@@ -79,7 +79,7 @@ describe('Project creation and compilation', function () {
const targetProjectName = `${sourceProjectName}-target`
login('user@example.com')
createProject(sourceProjectName, {
type: 'Example project',
type: 'Example Project',
open: false,
}).as('sourceProjectId')
createProject(targetProjectName).as('targetProjectId')
@@ -102,6 +102,10 @@ describe('Project creation and compilation', function () {
cy.findByText('Invite not yet accepted.')
})
cy.visit('/project')
cy.findByText('Account').click()
cy.findByText('Log Out').click()
login('collaborator@example.com')
openProjectViaInviteNotification(targetProjectName)
cy.get('@targetProjectId').then(targetProjectId => {
+1 -1
View File
@@ -20,7 +20,7 @@ services:
OVERLEAF_EMAIL_SMTP_HOST: 'mailtrap'
OVERLEAF_EMAIL_SMTP_PORT: '25'
OVERLEAF_EMAIL_SMTP_IGNORE_TLS: 'true'
ENABLED_LINKED_FILE_TYPES: 'project_file,project_output_file'
ENABLED_LINKED_FILE_TYPES: 'project_file,project_output_file,url'
ENABLE_CONVERSIONS: 'true'
EMAIL_CONFIRMATION_DISABLED: 'true'
healthcheck:
+137 -3
View File
@@ -1,7 +1,11 @@
import {
createNewFile,
createProject,
enableLinkSharing,
openFile,
openProjectById,
openProjectViaLinkSharingAsUser,
toggleTrackChanges,
} from './helpers/project'
import { isExcludedBySharding, startWith } from './helpers/config'
import { ensureUserExists, login } from './helpers/login'
@@ -22,7 +26,7 @@ describe('editor', () => {
beforeWithReRunOnTestRetry(function () {
projectName = `project-${uuid()}`
login('user@example.com')
createProject(projectName, { type: 'Example project', open: false }).then(
createProject(projectName, { type: 'Example Project', open: false }).then(
id => (projectId = id)
)
;({ recompile, waitForCompileRateLimitCoolOff } =
@@ -62,12 +66,12 @@ describe('editor', () => {
cy.log('add word to dictionary')
cy.get('.ol-cm-spelling-error').contains(word).rightclick()
cy.findByText('Add to dictionary').click()
cy.findByText('Add to Dictionary').click()
cy.get('.ol-cm-spelling-error').should('not.exist')
cy.log('remove word from dictionary')
cy.get('button').contains('Menu').click()
cy.get('button#dictionary-settings').contains('Edit').click()
cy.get('button').contains('Edit').click()
cy.get('[id="dictionary-modal"]').within(() => {
cy.findByText(word)
.parent()
@@ -88,6 +92,136 @@ describe('editor', () => {
})
})
describe('collaboration', () => {
beforeWithReRunOnTestRetry(function () {
enableLinkSharing().then(({ linkSharingReadAndWrite }) => {
const email = 'collaborator@example.com'
login(email)
openProjectViaLinkSharingAsUser(
linkSharingReadAndWrite,
projectName,
email
)
})
login('user@example.com')
waitForCompileRateLimitCoolOff(() => {
openProjectById(projectId)
})
})
it('track-changes', () => {
cy.log('disable track-changes before populating doc')
toggleTrackChanges(false)
const fileName = createNewFile()
const oldContent = 'oldContent'
cy.get('.cm-line').type(`${oldContent}\n\nstatic`)
cy.log('recompile to force flush')
recompile()
cy.log('enable track-changes for everyone')
toggleTrackChanges(true)
login('collaborator@example.com')
waitForCompileRateLimitCoolOff(() => {
openProjectById(projectId)
})
openFile(fileName, 'static')
cy.log('make changes in main file')
// cy.type() "clicks" in the center of the selected element before typing. This "click" discards the text as selected by the dblclick.
// Go down to the lower level event based typing, the frontend tests in web use similar events.
cy.get('.cm-editor').as('editor')
cy.get('@editor').findByText(oldContent).dblclick()
cy.get('@editor').trigger('keydown', { key: 'Delete' })
cy.get('@editor').trigger('keydown', { key: 'Enter' })
cy.get('@editor').trigger('keydown', { key: 'Enter' })
cy.log('recompile to force flush')
recompile()
login('user@example.com')
waitForCompileRateLimitCoolOff(() => {
openProjectById(projectId)
})
openFile(fileName, 'static')
cy.log('reject changes')
cy.contains('.toolbar-item', 'Review').click()
cy.get('.cm-content').should('not.contain.text', oldContent)
cy.findByText('Reject change').click({ force: true })
cy.contains('.toolbar-item', 'Review').click()
cy.log('recompile to force flush')
recompile()
cy.log('verify the changes are applied')
cy.get('.cm-content').should('contain.text', oldContent)
cy.log('disable track-changes for everyone again')
toggleTrackChanges(false)
})
it('track-changes rich text', () => {
cy.log('disable track-changes before populating doc')
toggleTrackChanges(false)
const fileName = createNewFile()
const oldContent = 'oldContent'
cy.get('.cm-line').type(`\\section{{}${oldContent}}\n\nstatic`)
cy.log('recompile to force flush')
recompile()
cy.log('enable track-changes for everyone')
toggleTrackChanges(true)
login('collaborator@example.com')
waitForCompileRateLimitCoolOff(() => {
openProjectById(projectId)
})
cy.log('enable visual editor and make changes in main file')
cy.findByText('Visual Editor').click()
openFile(fileName, 'static')
// cy.type() "clicks" in the center of the selected element before typing. This "click" discards the text as selected by the dblclick.
// Go down to the lower level event based typing, the frontend tests in web use similar events.
cy.get('.cm-editor').as('editor')
cy.get('@editor').findByText(oldContent).dblclick()
cy.get('@editor').trigger('keydown', { key: 'Delete' })
cy.get('@editor').trigger('keydown', { key: 'Enter' })
cy.get('@editor').trigger('keydown', { key: 'Enter' })
cy.log('recompile to force flush')
recompile()
login('user@example.com')
waitForCompileRateLimitCoolOff(() => {
openProjectById(projectId)
})
openFile(fileName, 'static')
cy.log('reject changes')
cy.contains('.toolbar-item', 'Review').click()
cy.get('.cm-content').should('not.contain.text', oldContent)
cy.findAllByText('Reject change').first().click({ force: true })
cy.contains('.toolbar-item', 'Review').click()
cy.log('recompile to force flush')
recompile()
cy.log('verify the changes are applied in the visual editor')
cy.findByText('Visual Editor').click()
cy.get('.cm-content').should('contain.text', oldContent)
cy.log('disable track-changes for everyone again')
toggleTrackChanges(false)
})
})
describe('editor', () => {
it('renders jpg', () => {
cy.findByTestId('file-tree').findByText('frog.jpg').click()
+6 -6
View File
@@ -46,7 +46,7 @@ describe('git-bridge', function () {
function maybeClearAllTokens() {
cy.visit('/user/settings')
cy.findByText('Git integration')
cy.findByText('Git Integration')
cy.get('button')
.contains(/Generate token|Add another token/)
.then(btn => {
@@ -63,7 +63,7 @@ describe('git-bridge', function () {
it('should render the git-bridge UI in the settings', () => {
maybeClearAllTokens()
cy.visit('/user/settings')
cy.findByText('Git integration')
cy.findByText('Git Integration')
cy.get('button').contains('Generate token').click()
cy.get('code')
.contains(/olp_[a-zA-Z0-9]{16}/)
@@ -93,7 +93,7 @@ describe('git-bridge', function () {
cy.get('code').contains(`git clone ${gitURL(id.toString())}`)
})
cy.findByRole('button', {
name: /generate token/i,
name: 'Generate token',
}).click()
cy.get('code').contains(/olp_[a-zA-Z0-9]{16}/)
})
@@ -107,7 +107,7 @@ describe('git-bridge', function () {
cy.get('code').contains(`git clone ${gitURL(id.toString())}`)
})
cy.findByText('Generate token').should('not.exist')
cy.findByText(/generate a new one in Account settings/i)
cy.findByText(/generate a new one in Account Settings/)
cy.findByText('Go to settings')
.should('have.attr', 'target', '_blank')
.and('have.attr', 'href', '/user/settings')
@@ -196,7 +196,7 @@ describe('git-bridge', function () {
cy.get('code').contains(`git clone ${gitURL(projectId.toString())}`)
})
cy.findByRole('button', {
name: /generate token/i,
name: 'Generate token',
}).click()
cy.get('code')
.contains(/olp_[a-zA-Z0-9]{16}/)
@@ -365,7 +365,7 @@ Hello world
it('should not render the git-bridge UI in the settings', () => {
login('user@example.com')
cy.visit('/user/settings')
cy.findByText('Git integration').should('not.exist')
cy.findByText('Git Integration').should('not.exist')
})
it('should not render the git-bridge UI in the editor', function () {
login('user@example.com')
+37 -3
View File
@@ -5,11 +5,11 @@ import { v4 as uuid } from 'uuid'
export function createProject(
name: string,
{
type = 'Blank project',
type = 'Blank Project',
newProjectButtonMatcher = /new project/i,
open = true,
}: {
type?: 'Blank project' | 'Example project'
type?: 'Blank Project' | 'Example Project'
newProjectButtonMatcher?: RegExp
open?: boolean
} = {}
@@ -37,7 +37,7 @@ export function createProject(
}
cy.findAllByRole('button').contains(newProjectButtonMatcher).click()
// FIXME: This should only look in the left menu
cy.findAllByText(new RegExp(type, 'i')).first().click()
cy.findAllByText(type).first().click()
cy.findByRole('dialog').within(() => {
cy.get('input').type(name)
cy.findByText('Create').click()
@@ -215,3 +215,37 @@ export function createNewFile() {
return fileName
}
export function toggleTrackChanges(state: boolean) {
cy.findByText('Review').click()
cy.get('.track-changes-menu-button').then(el => {
// when the menu is expanded renders the `expand_more` icon,
// and the `chevron_right` icon when it's collapsed
if (!el.text().includes('expand_more')) {
el.click()
}
})
cy.findByText('Everyone')
.parent()
.within(() => {
cy.get('.form-check-input').then(el => {
if (el.prop('checked') === state) return
const id = uuid()
const alias = `@${id}`
cy.intercept({
method: 'POST',
url: '**/track_changes',
times: 1,
}).as(id)
if (state) {
cy.get('.form-check-input').check()
} else {
cy.get('.form-check-input').uncheck()
}
cy.wait(alias)
})
})
cy.contains('.toolbar-item', 'Review').click()
}
+6 -1
View File
@@ -131,7 +131,9 @@ const allowedVars = Joi.object(
'GIT_BRIDGE_HOST',
'GIT_BRIDGE_PORT',
'V1_HISTORY_URL',
'DOCKER_RUNNER',
'SANDBOXED_COMPILES',
'SANDBOXED_COMPILES_SIBLING_CONTAINERS',
'ALL_TEX_LIVE_DOCKER_IMAGE_NAMES',
'OVERLEAF_TEMPLATES_USER_ID',
'OVERLEAF_NEW_PROJECT_TEMPLATE_LINKS',
@@ -194,7 +196,10 @@ function setVarsDockerCompose({ pro, vars, version, withDataDir }) {
)
}
if (cfg.services.sharelatex.environment.SANDBOXED_COMPILES === 'true') {
if (
cfg.services.sharelatex.environment
.SANDBOXED_COMPILES_SIBLING_CONTAINERS === 'true'
) {
cfg.services.sharelatex.environment.SANDBOXED_COMPILES_HOST_DIR =
PATHS.SANDBOXED_COMPILES_HOST_DIR
cfg.services.sharelatex.environment.TEX_LIVE_DOCKER_IMAGE =
+1 -1
View File
@@ -32,7 +32,7 @@ describe('Project List', () => {
before(() => {
login(REGULAR_USER)
createProject(projectName, { type: 'Example project', open: false })
createProject(projectName, { type: 'Example Project', open: false })
})
beforeEach(function () {
login(REGULAR_USER)
+3 -3
View File
@@ -10,7 +10,9 @@ const LABEL_TEX_LIVE_VERSION = 'TeX Live version'
describe('SandboxedCompiles', function () {
const enabledVars = {
DOCKER_RUNNER: 'true',
SANDBOXED_COMPILES: 'true',
SANDBOXED_COMPILES_SIBLING_CONTAINERS: 'true',
ALL_TEX_LIVE_DOCKER_IMAGE_NAMES: '2023,2022',
}
@@ -59,9 +61,7 @@ describe('SandboxedCompiles', function () {
})
function checkSyncTeX() {
// TODO(25342): re-enable
// eslint-disable-next-line mocha/no-skipped-tests
describe.skip('SyncTeX', function () {
describe('SyncTeX', function () {
let projectName: string
beforeEach(function () {
projectName = `Project ${uuid()}`

Some files were not shown because too many files have changed in this diff Show More