[CE/SP] Hotfix 6.2.2 (#35546)

* [CE/SP] Hotfix 6.2.2

GitOrigin-RevId: 5ee457a611fe7d6f73d8ce4bcf55378bd8c7fc69
This commit is contained in:
Miguel Serrano
2026-09-17 11:32:08 +00:00
committed by Copybot
parent f27b19f86a
commit b0015ef166
9 changed files with 771 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
FROM sharelatex/sharelatex:6.2.1
# Apply security updates to base image
RUN apt update && apt install -y linux-libc-dev \
&& unattended-upgrade --verbose --no-minimal-upgrade-steps \
&& rm -rf /var/lib/apt/lists/*
# bump protobufjs to 7.6.3 and @protobufjs/* packages
COPY pr_33656.patch .
RUN patch -p1 < pr_33656.patch && rm pr_33656.patch
# bump form-data to 2.5.6
COPY pr_34920.patch .
RUN patch -p1 < pr_34920.patch && rm pr_34920.patch
# bump @grpc/grpc-js to 1.14.4
COPY pr_34580.patch .
RUN patch -p1 < pr_34580.patch && rm pr_34580.patch
# bump multer to 2.2.0
COPY pr_34916.patch .
RUN patch -p1 < pr_34916.patch && rm pr_34916.patch
# fix projectInviteEncryptorOptions assignment in settings.js
COPY fix_invite_token_secret.patch .
RUN patch -p1 < fix_invite_token_secret.patch && rm fix_invite_token_secret.patch
# gate group link sharing behind feature flag, remove debug logging
COPY pr_34394.patch .
RUN patch -p1 < pr_34394.patch && rm pr_34394.patch
# migrate email sender from @aws-sdk/client-ses to @aws-sdk/client-sesv2
COPY pr_35531.patch .
RUN patch -p1 < pr_35531.patch && rm pr_35531.patch
# add script for flushing/discarding stuck docs
COPY pr_35603.patch .
RUN patch -p1 < pr_35603.patch && rm pr_35603.patch
RUN yarn workspace @overleaf/web remove @aws-sdk/client-ses \
&& yarn workspace @overleaf/web add @aws-sdk/client-sesv2@3.994.0 \
&& yarn install --no-immutable \
&& node genScript compile | bash \
&& yarn workspaces focus --all --production
@@ -0,0 +1,13 @@
diff --git a/server-ce/config/settings.js b/server-ce/config/settings.js
index 160a0b8cb01..3ecb620a339 100644
--- a/server-ce/config/settings.js
+++ b/server-ce/config/settings.js
@@ -307,7 +307,7 @@ const settings = {
// This secret is used for encrypting sharing link tokens in the database
if (process.env.OVERLEAF_INVITE_TOKEN_SECRET) {
- module.exports.projectInviteEncryptorOptions = {
+ settings.projectInviteEncryptorOptions = {
cipherLabel: '2026.3-v3',
cipherPasswords: {
'2026.3-v3': process.env.OVERLEAF_INVITE_TOKEN_SECRET,
+132
View File
@@ -0,0 +1,132 @@
diff --git a/package.json b/package.json
index fff7e1383f32..ac93e9ebfbc1 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,8 @@
"@cypress/request/qs": "6.14.1",
"@opentelemetry/api": "1.9.0",
"mocha/serialize-javascript": "7.0.5",
- "pprof/protobufjs": "7.5.5",
- "@google-cloud/profiler/protobufjs": "7.5.5",
+ "protobufjs": "7.6.3",
+ "@protobufjs/utf8": "1.1.1",
"mocha-multi-reporters": "patch:mocha-multi-reporters@npm%3A1.5.1#~/.yarn/patches/mocha-multi-reporters-npm-1.5.1-0a1088aed5.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A5.1.91#~/.yarn/patches/pdfjs-dist-npm-5.1.91.patch",
"referer-parser": "patch:referer-parser@npm%3A0.0.3#~/.yarn/patches/referer-parser-npm-0.0.3.patch",
diff --git a/yarn.lock b/yarn.lock
index f6e71513d7a3..03cadecb9882 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8098,27 +8098,26 @@ __metadata:
languageName: node
linkType: hard
-"@protobufjs/codegen@npm:^2.0.4":
- version: 2.0.4
- resolution: "@protobufjs/codegen@npm:2.0.4"
- checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43
+"@protobufjs/codegen@npm:^2.0.5":
+ version: 2.0.5
+ resolution: "@protobufjs/codegen@npm:2.0.5"
+ checksum: 10c0/1b8a2ae56ee60a56e9d205cd4b6072a1503c5069b8ebb905710f974ff0098a0d0700641c137e0a8d98dedf14423156a106a9433695cbf52574810f55000fdcab
languageName: node
linkType: hard
-"@protobufjs/eventemitter@npm:^1.1.0":
- version: 1.1.0
- resolution: "@protobufjs/eventemitter@npm:1.1.0"
- checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70
+"@protobufjs/eventemitter@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@protobufjs/eventemitter@npm:1.1.1"
+ checksum: 10c0/8e06193d4629c5e7c09d4f8c2ddba8fc4dfa739f0149f33a1d901568d35bb7b8b5277a4e8452baf3bdd0b302fd599cf255d193267aa93a0a4747e23cd073c4ac
languageName: node
linkType: hard
-"@protobufjs/fetch@npm:^1.1.0":
- version: 1.1.0
- resolution: "@protobufjs/fetch@npm:1.1.0"
+"@protobufjs/fetch@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@protobufjs/fetch@npm:1.1.1"
dependencies:
"@protobufjs/aspromise": "npm:^1.1.1"
- "@protobufjs/inquire": "npm:^1.1.0"
- checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233
+ checksum: 10c0/a497ff5433854e8577f0427983ea39b9113b49a8120f94515291d763327061d2c3013e60e24ea436d091dafae01a0f6eb1867e3b1616045d96a31d8b3c646ed4
languageName: node
linkType: hard
@@ -8129,10 +8128,10 @@ __metadata:
languageName: node
linkType: hard
-"@protobufjs/inquire@npm:^1.1.0":
- version: 1.1.0
- resolution: "@protobufjs/inquire@npm:1.1.0"
- checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a
+"@protobufjs/inquire@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "@protobufjs/inquire@npm:1.1.2"
+ checksum: 10c0/af69597818a14cac6a00a74cd5b3fb85aa96658b9dbbae73e6d907cb154ebf470953a8c537b3e6095a43de565ec4e6c5b40227c72f4a7d762d34fbec7ac179e7
languageName: node
linkType: hard
@@ -8150,10 +8149,10 @@ __metadata:
languageName: node
linkType: hard
-"@protobufjs/utf8@npm:^1.1.0":
- version: 1.1.0
- resolution: "@protobufjs/utf8@npm:1.1.0"
- checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487
+"@protobufjs/utf8@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@protobufjs/utf8@npm:1.1.1"
+ checksum: 10c0/641fc145f00626405e8984b6e90b9edcbcc072ffc82d0647ca3176e09c730b2d022f988e65f011a7a17e2e4d77cde7733643aa10d8ac2bfa30f134dbcad553fd
languageName: node
linkType: hard
@@ -23208,7 +23207,7 @@ __metadata:
languageName: node
linkType: hard
-"long@npm:^5.0.0":
+"long@npm:^5.0.0, long@npm:^5.3.2":
version: 5.3.2
resolution: "long@npm:5.3.2"
checksum: 10c0/7130fe1cbce2dca06734b35b70d380ca3f70271c7f8852c922a7c62c86c4e35f0c39290565eca7133c625908d40e126ac57c02b1b1a4636b9457d77e1e60b981
@@ -28120,23 +28119,23 @@ __metadata:
languageName: node
linkType: hard
-"protobufjs@npm:7.5.5, protobufjs@npm:^7.2.5, protobufjs@npm:^7.3.2, protobufjs@npm:^7.5.3":
- version: 7.5.5
- resolution: "protobufjs@npm:7.5.5"
+"protobufjs@npm:7.6.3":
+ version: 7.6.3
+ resolution: "protobufjs@npm:7.6.3"
dependencies:
"@protobufjs/aspromise": "npm:^1.1.2"
"@protobufjs/base64": "npm:^1.1.2"
- "@protobufjs/codegen": "npm:^2.0.4"
- "@protobufjs/eventemitter": "npm:^1.1.0"
- "@protobufjs/fetch": "npm:^1.1.0"
+ "@protobufjs/codegen": "npm:^2.0.5"
+ "@protobufjs/eventemitter": "npm:^1.1.1"
+ "@protobufjs/fetch": "npm:^1.1.1"
"@protobufjs/float": "npm:^1.0.2"
- "@protobufjs/inquire": "npm:^1.1.0"
+ "@protobufjs/inquire": "npm:^1.1.2"
"@protobufjs/path": "npm:^1.1.2"
"@protobufjs/pool": "npm:^1.1.0"
- "@protobufjs/utf8": "npm:^1.1.0"
+ "@protobufjs/utf8": "npm:^1.1.1"
"@types/node": "npm:>=13.7.0"
- long: "npm:^5.0.0"
- checksum: 10c0/3d48896a916761e3e60b52f80027eb4fba3f5a6e3f8461e04939db18812db2cb0db4c73d03e1134a960e99525ae1d236f076a0bc01273016f573b76f33ffbd47
+ long: "npm:^5.3.2"
+ checksum: 10c0/e1132b1ff82684280041550d720398568763ff40928def03e8bb409eaed6edd222e63372e3f52d7c19e5a8e9975333480e26950759086eb5a1f5b5c118da7bf8
languageName: node
linkType: hard
+106
View File
@@ -0,0 +1,106 @@
diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHelper.mjs b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHelper.mjs
index 8a772a17412b..4b231e0614cc 100644
--- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHelper.mjs
+++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHelper.mjs
@@ -43,7 +43,6 @@ async function encryptToken(token) {
if (!accessTokenEncryptor) {
throw new Error('Token encryption not configured, could not encrypt token')
}
- logger.debug({ settings: Settings.projectInviteEncryptorOptions })
return accessTokenEncryptor.promises.encryptJson(token)
}
@@ -51,7 +50,6 @@ async function decryptToken(encryptedToken) {
if (!accessTokenEncryptor) {
throw new Error('Token encryption not configured, could not encrypt token')
}
- logger.debug({ settings: Settings.projectInviteEncryptorOptions })
return accessTokenEncryptor.promises.decryptToJson(encryptedToken)
}
diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs
index bce4a5699aea..6ace94a5a687 100644
--- a/services/web/app/src/Features/Project/ProjectController.mjs
+++ b/services/web/app/src/Features/Project/ProjectController.mjs
@@ -495,6 +495,7 @@ const _ProjectController = {
'editor-upgrade-button-relocation',
'markdown-visual',
'ai-disabled-collaborators',
+ 'group-link-sharing',
].filter(Boolean)
const getUserValues = async userId =>
diff --git a/services/web/frontend/js/features/share-project-modal/components/project-access.tsx b/services/web/frontend/js/features/share-project-modal/components/project-access.tsx
index 512b8f080a6d..847663bbf679 100644
--- a/services/web/frontend/js/features/share-project-modal/components/project-access.tsx
+++ b/services/web/frontend/js/features/share-project-modal/components/project-access.tsx
@@ -29,6 +29,7 @@ import {
} from '@/features/share-project-modal/components/share-project-modal'
import { ExcludeStrict } from '@ol-types/utils'
import getMeta from '@/utils/meta'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
type ProjectAccessProps = {
setIsInvitedPeopleScreen: React.Dispatch<React.SetStateAction<boolean>>
@@ -50,6 +51,7 @@ function ProjectAccess({
)
const { isProjectOwner } = useEditorContext()
const { activeGroupSubscriptions } = getMeta('ol-user')
+ const groupSharingEnabled = useFeatureFlag('group-link-sharing')
const {
monitorRequest,
@@ -235,7 +237,8 @@ function ProjectAccess({
{t('only_invited_people')}
</DropdownItem>
</DropdownListItem>
- {activeGroupSubscriptions &&
+ {groupSharingEnabled &&
+ activeGroupSubscriptions &&
activeGroupSubscriptions.map(subscription => (
<DropdownListItem
className="d-flex align-items-center"
diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx
index 4ce9b78d46c2..9e397a1bb31d 100644
--- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx
+++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.tsx
@@ -7,7 +7,10 @@ import React, {
} from 'react'
import ShareProjectModalContent from './share-project-modal-content'
import { useProjectContext } from '@/shared/context/project-context'
-import { useSplitTestContext } from '@/shared/context/split-test-context'
+import {
+ useFeatureFlag,
+ useSplitTestContext,
+} from '@/shared/context/split-test-context'
import { sendMB } from '@/infrastructure/event-tracking'
import { useEditorContext } from '@/shared/context/editor-context'
import customLocalStorage from '@/infrastructure/local-storage'
@@ -93,6 +96,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
const { publicAccessLevel } = project || {}
const { splitTestVariants } = useSplitTestContext()
+ const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
// show the new share modal if project owner
// is over collaborator limit or has pending editors (once every 24 hours)
@@ -147,6 +151,10 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
}, [show])
const handleShow = useCallback(async () => {
+ if (!isSharingUpdatesEnabled || !isProjectOwner) {
+ return
+ }
+
if (publicAccessLevel === 'tokenBased') {
setSharingLinkData(null)
setProjectAccess('legacyLinkSharing')
@@ -179,7 +187,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
'generic_something_went_wrong'
)
}
- }, [publicAccessLevel, projectId])
+ }, [publicAccessLevel, projectId, isSharingUpdatesEnabled, isProjectOwner])
// close the modal if not in flight
const cancel = useCallback(() => {
+20
View File
@@ -0,0 +1,20 @@
diff --git a/yarn.lock b/yarn.lock
index 8c85b5b32545..a5ce4cdf9bbb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3983,12 +3983,12 @@ __metadata:
linkType: hard
"@grpc/grpc-js@npm:^1.1.8, @grpc/grpc-js@npm:^1.10.9, @grpc/grpc-js@npm:^1.11.1, @grpc/grpc-js@npm:^1.14.3":
- version: 1.14.3
- resolution: "@grpc/grpc-js@npm:1.14.3"
+ version: 1.14.4
+ resolution: "@grpc/grpc-js@npm:1.14.4"
dependencies:
"@grpc/proto-loader": "npm:^0.8.0"
"@js-sdsl/ordered-map": "npm:^4.4.2"
- checksum: 10c0/f41f06a311b93cca8c472d56e21387e0f7b57bb2337a91d15ea4279bac8ec4fa0de6bd0d881201229ab800c0f0c55277911ecb850e057f20a828d0ddd623551d
+ checksum: 10c0/0ff6395e8112ad30e8f99dbb684b997ebc3264e770b8e354f23effeedf181a380e0ecef8bca466cbbf3e9141968656144851de1da50f840a1efd9314c9812449
languageName: node
linkType: hard
+114
View File
@@ -0,0 +1,114 @@
diff --git a/.yarn/patches/multer-npm-2.1.1.patch b/.yarn/patches/multer-npm-2.2.0-4ed181c78f.patch
similarity index 77%
rename from .yarn/patches/multer-npm-2.1.1.patch
rename to .yarn/patches/multer-npm-2.2.0-4ed181c78f.patch
index c74230a10835..10e08cf24327 100644
--- a/.yarn/patches/multer-npm-2.1.1.patch
+++ b/.yarn/patches/multer-npm-2.2.0-4ed181c78f.patch
@@ -1,8 +1,8 @@
diff --git a/lib/make-middleware.js b/lib/make-middleware.js
-index ee50988..de77364 100644
+index 2c15f248d4f4d221bd9e2b2e1092b8d5c46d2bdd..f2bceb4479c94e321d4882ca1fe09c3ad280cca7 100644
--- a/lib/make-middleware.js
+++ b/lib/make-middleware.js
-@@ -164,7 +164,7 @@ function makeMiddleware (setup) {
+@@ -174,7 +174,7 @@ function makeMiddleware (setup) {
if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
// don't attach to the files object, if there is no file
diff --git a/package.json b/package.json
index 2e77c8097eeb..7d7230d92ffd 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,7 @@
"body-parser@npm:1.20.4": "patch:body-parser@npm%3A1.20.4#~/.yarn/patches/body-parser-npm-1.20.4.patch",
"cypress-multi-reporters": "patch:cypress-multi-reporters@npm%3A2.0.5#~/.yarn/patches/cypress-multi-reporters-npm-2.0.5.patch",
"forwarded@npm:0.2.0": "patch:forwarded@npm%3A0.2.0#~/.yarn/patches/forwarded-npm-0.2.0.patch",
- "multer@npm:2.1.1": "patch:multer@npm%3A2.1.1#~/.yarn/patches/multer-npm-2.1.1.patch",
+ "multer@npm:2.2.0": "patch:multer@npm%3A2.2.0#~/.yarn/patches/multer-npm-2.2.0-4ed181c78f.patch",
"node-fetch": "patch:node-fetch@npm%3A2.7.0#~/.yarn/patches/node-fetch-npm-2.7.0.patch",
"passport-oauth2": "patch:passport-oauth2@npm%3A1.6.1#~/.yarn/patches/passport-oauth2-npm-1.6.1.patch",
"send": "patch:send@npm%3A0.19.0#~/.yarn/patches/send-npm-0.19.0.patch",
diff --git a/services/clsi/package.json b/services/clsi/package.json
index e2bcc927ed6b..edb8d65bc96d 100644
--- a/services/clsi/package.json
+++ b/services/clsi/package.json
@@ -32,6 +32,6 @@
"express": "4.22.1",
"lodash": "^4.18.1",
- "multer": "2.1.1",
+ "multer": "2.2.0",
"overleaf-editor-core": "workspace:*",
"p-limit": "^3.1.0",
"send": "^0.19.0",
diff --git a/services/web/package.json b/services/web/package.json
index 1e3dd2acc41c..4ad8b3593c75 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -154,7 +154,7 @@
"moment": "^2.29.4",
"mongodb-legacy": "6.1.3",
"mongoose": "8.22.1",
- "multer": "2.1.1",
+ "multer": "2.2.0",
"nocache": "^2.1.0",
"node-fetch": "^2.7.0",
"nodemailer": "^8.0.5",
diff --git a/yarn.lock b/yarn.lock
index 371bfa1a9363..d60a273925b4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6705,7 +6705,7 @@ __metadata:
mocha: "npm:^11.1.0"
mocha-junit-reporter: "npm:^2.2.1"
mocha-multi-reporters: "npm:^1.5.1"
- multer: "npm:2.1.1"
+ multer: "npm:2.2.0"
node-fetch: "npm:^2.7.0"
nyc: "npm:^17.1.0"
overleaf-editor-core: "workspace:*"
@@ -7679,7 +7679,7 @@ __metadata:
moment: "npm:^2.29.4"
mongodb-legacy: "npm:6.1.3"
mongoose: "npm:8.22.1"
- multer: "npm:2.1.1"
+ multer: "npm:2.2.0"
nocache: "npm:^2.1.0"
nock: "npm:^13.5.6"
node-fetch: "npm:^2.7.0"
@@ -25279,27 +25279,27 @@ __metadata:
languageName: node
linkType: hard
-"multer@npm:2.1.1":
- version: 2.1.1
- resolution: "multer@npm:2.1.1"
+"multer@npm:2.2.0":
+ version: 2.2.0
+ resolution: "multer@npm:2.2.0"
dependencies:
append-field: "npm:^1.0.0"
busboy: "npm:^1.6.0"
concat-stream: "npm:^2.0.0"
type-is: "npm:^1.6.18"
- checksum: 10c0/2ec4e02833b20f403cfb879d4b64d2a9070d902b9deae7aef18a6faadb707d7665385456cf540aa8a6dadfe3d4c5fc8e0e7b0675b94e1077048b1125426deee6
+ checksum: 10c0/7aa366d89042427347b6ab8e4a203405d7fe942e4a3095648a14526fcf3691d98ffc2da62abf85d8aca0a341f828168eb197b33cfdcfcce29d6ef593a5625591
languageName: node
linkType: hard
-"multer@patch:multer@npm%3A2.1.1#~/.yarn/patches/multer-npm-2.1.1.patch":
- version: 2.1.1
- resolution: "multer@patch:multer@npm%3A2.1.1#~/.yarn/patches/multer-npm-2.1.1.patch::version=2.1.1&hash=0a6fed"
+"multer@patch:multer@npm%3A2.2.0#~/.yarn/patches/multer-npm-2.2.0-4ed181c78f.patch":
+ version: 2.2.0
+ resolution: "multer@patch:multer@npm%3A2.2.0#~/.yarn/patches/multer-npm-2.2.0-4ed181c78f.patch::version=2.2.0&hash=cf9e1c"
dependencies:
append-field: "npm:^1.0.0"
busboy: "npm:^1.6.0"
concat-stream: "npm:^2.0.0"
type-is: "npm:^1.6.18"
- checksum: 10c0/647285a1c53531143d44d54ce923d3fca2037f368e581c9eecfa8b40aa5a8ef706fb1ed2a5635228002c38e31861d6209ab33a07d194c8768e7ae113b7a67976
+ checksum: 10c0/9aa266ba976e6ee0d292d4b838d7e957086bcfb53fa170b62ca26d8737cc5eb2fc066fba0045895cbce6981a467adc0f801685b5528e8d1d09e00ca1544163dc
languageName: node
linkType: hard
+12
View File
@@ -0,0 +1,12 @@
--- a/package.json
+++ b/package.json
@@ -40,7 +40,8 @@
"referer-parser/js-yaml": "^4.1.1",
"sandboxed-module": "patch:sandboxed-module@npm%3A2.0.4#~/.yarn/patches/sandboxed-module-npm-2.0.4-f8b45aacc9.patch",
"request/tough-cookie": "5.1.2",
- "request/form-data": "2.5.5",
+ "request/form-data": "2.5.6",
+ "@types/request/form-data": "2.5.6",
"request/qs": "6.14.1",
"@cypress/request/qs": "6.14.1",
"@opentelemetry/api": "1.9.0",
+31
View File
@@ -0,0 +1,31 @@
diff --git a/services/web/app/src/Features/Email/EmailSender.mjs b/services/web/app/src/Features/Email/EmailSender.mjs
index 5eaef54202a..d5fbe797a46 100644
--- a/services/web/app/src/Features/Email/EmailSender.mjs
+++ b/services/web/app/src/Features/Email/EmailSender.mjs
@@ -3,7 +3,7 @@ import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import Settings from '@overleaf/settings'
import nodemailer from 'nodemailer'
-import * as aws from '@aws-sdk/client-ses'
+import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'
import OError from '@overleaf/o-error'
import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
import _ from 'lodash'
@@ -30,15 +30,14 @@ function getClient() {
const emailParameters = EMAIL_SETTINGS.parameters
if (emailParameters.AWSAccessKeyID || EMAIL_SETTINGS.driver === 'ses') {
logger.debug('using aws ses for email')
- const ses = new aws.SESClient({
- apiVersion: '2010-12-01',
+ const sesClient = new SESv2Client({
region: emailParameters.region,
credentials: {
accessKeyId: emailParameters.AWSAccessKeyID,
secretAccessKey: emailParameters.AWSSecretKey,
},
})
- client = nodemailer.createTransport({ SES: { ses, aws } })
+ client = nodemailer.createTransport({ SES: { sesClient, SendEmailCommand } })
} else if (emailParameters.sendgridApiKey) {
throw new OError(
'sendgridApiKey configuration option is deprecated, use SMTP instead'
+299
View File
@@ -0,0 +1,299 @@
diff --git a/services/document-updater/app/js/ProjectFlusher.js b/services/document-updater/app/js/ProjectFlusher.js
index d6b8c5b13304..fb6687417a64 100644
--- a/services/document-updater/app/js/ProjectFlusher.js
+++ b/services/document-updater/app/js/ProjectFlusher.js
@@ -96,6 +96,7 @@ async function flushAllProjects(options) {
}
module.exports = {
+ _getKeys,
_extractIds,
promises: {
flushAllProjects,
diff --git a/services/document-updater/scripts/flush_docs_with_pending_updates.js b/services/document-updater/scripts/flush_docs_with_pending_updates.js
new file mode 100644
index 000000000000..1be0f3c6acf1
--- /dev/null
+++ b/services/document-updater/scripts/flush_docs_with_pending_updates.js
@@ -0,0 +1,281 @@
+// Flush docs that were left with a stuck PendingUpdates queue in redis after
+// their project was flushed (and possibly hard deleted since).
+//
+// For each doc with a PendingUpdates queue, the project id is looked up in
+// db.docs, then the pending updates are processed and the doc is flushed and
+// removed from redis. Docs whose project cannot be found (e.g. hard deleted
+// projects) are logged and can be discarded with --discard-hard-deleted.
+
+const minimist = require('minimist')
+const Settings = require('@overleaf/settings')
+const OError = require('@overleaf/o-error')
+const DocumentManager = require('../app/js/DocumentManager')
+const HistoryManager = require('../app/js/HistoryManager')
+const ProjectFlusher = require('../app/js/ProjectFlusher')
+const { rclient } = require('../app/js/RedisManager')
+const { db, ObjectId, mongoClient } = require('../app/js/mongodb')
+
+const keys = Settings.redis.documentupdater.key_schema
+
+const summary = {
+ totalDocs: 0,
+ flushedDocs: 0,
+ missingDocs: 0,
+ discardedDocs: 0,
+ erroredDocs: 0,
+}
+
+function printSummary() {
+ console.log(
+ `Processed ${summary.totalDocs} docs with pending updates: flushed=${summary.flushedDocs} missing=${summary.missingDocs} discarded=${summary.discardedDocs} errored=${summary.erroredDocs}`
+ )
+}
+
+function usage() {
+ console.error(`
+Usage: node scripts/flush_docs_with_pending_updates.js [options]
+
+Options:
+ --estimated-docs Required. Estimated number of documents in db.docs,
+ used as a safety check that the right database is
+ consulted for the doc lookups. The script aborts
+ unless the estimated collection count is within 20%
+ of this value.
+ --log-unflushed When the db.docs lookup failed for a doc, log its
+ pending updates and, if the doc has unflushed
+ changes, all its doc keys
+ (default: true, disable via --log-unflushed=false)
+ --discard-hard-deleted Discard the stuck queue and the other doc keys of
+ docs whose db.docs lookup failed (default: false)
+ --docId Process a single doc instead of scanning redis
+ --projectId Override the project id for --docId, skipping the
+ db.docs lookup
+ --dry-run Only scan and log, without flushing, deleting or
+ discarding anything (default: false)
+ --limit Maximum number of PendingUpdates keys to scan
+ (default: 100000)
+ --help Show this help message
+`)
+}
+
+async function getDocIds(options) {
+ if (options.docId) {
+ return [options.docId]
+ }
+ const docKeys = await ProjectFlusher._getKeys(
+ keys.pendingUpdates({ doc_id: '*' }),
+ options.limit
+ )
+ return ProjectFlusher._extractIds(docKeys)
+}
+
+async function processDoc(docId, options) {
+ let projectId = options.projectId
+ if (!projectId) {
+ const doc = await db.docs.findOne(
+ { _id: new ObjectId(docId) },
+ { projection: { project_id: 1 } }
+ )
+ projectId = doc?.project_id?.toString()
+ }
+
+ if (projectId) {
+ await flushDoc(projectId, docId, options)
+ } else {
+ await handleMissingDoc(docId, options)
+ }
+}
+
+async function flushDoc(projectId, docId, options) {
+ const queueLength = await rclient.llen(keys.pendingUpdates({ doc_id: docId }))
+ if (options.dryRun) {
+ console.log(
+ `Would flush doc ${docId} in project ${projectId} with ${queueLength} pending updates`
+ )
+ return
+ }
+ console.log(
+ `Flushing doc ${docId} in project ${projectId} with ${queueLength} pending updates`
+ )
+ await DocumentManager.promises.flushAndDeleteDocWithLock(projectId, docId, {})
+ await HistoryManager.promises.flushProjectChanges(projectId, {})
+ const remaining = await rclient.llen(keys.pendingUpdates({ doc_id: docId }))
+ if (remaining > 0) {
+ console.error(
+ `Doc ${docId} in project ${projectId} still has ${remaining} pending updates after flushing`
+ )
+ summary.erroredDocs++
+ } else {
+ summary.flushedDocs++
+ }
+}
+
+async function handleMissingDoc(docId, options) {
+ summary.missingDocs++
+ let message = `Doc ${docId} not found in db.docs`
+
+ if (options.logUnflushed) {
+ // Log the pending updates: they are the reason this doc is processed and
+ // would be lost when discarding it. These can exist independently of any
+ // unflushed doc content.
+ const pendingUpdates = await rclient.lrange(
+ keys.pendingUpdates({ doc_id: docId }),
+ 0,
+ -1
+ )
+ message += `, pending updates: ${JSON.stringify(pendingUpdates)}`
+
+ // In addition, log the unflushed doc content if there is any.
+ const unflushedTime = await rclient.get(
+ keys.unflushedTime({ doc_id: docId })
+ )
+ if (unflushedTime) {
+ const doc = {
+ unflushedTime,
+ projectId: await rclient.get(keys.projectKey({ doc_id: docId })),
+ lines: await rclient.get(keys.docLines({ doc_id: docId })),
+ version: await rclient.get(keys.docVersion({ doc_id: docId })),
+ hash: await rclient.get(keys.docHash({ doc_id: docId })),
+ ranges: await rclient.get(keys.ranges({ doc_id: docId })),
+ pathname: await rclient.get(keys.pathname({ doc_id: docId })),
+ projectHistoryId: await rclient.get(
+ keys.projectHistoryId({ doc_id: docId })
+ ),
+ lastUpdatedAt: await rclient.get(keys.lastUpdatedAt({ doc_id: docId })),
+ lastUpdatedBy: await rclient.get(keys.lastUpdatedBy({ doc_id: docId })),
+ resolvedCommentIds: await rclient.smembers(
+ keys.resolvedCommentIds({ doc_id: docId })
+ ),
+ }
+ message += `, unflushed doc keys: ${JSON.stringify(doc)}`
+ }
+ }
+ console.warn(message)
+
+ if (options.discardHardDeleted && !options.dryRun) {
+ await discardDoc(docId)
+ console.warn(`Discarded redis keys of doc ${docId}`)
+ summary.discardedDocs++
+ }
+}
+
+async function discardDoc(docId) {
+ // Purge the project-level entries first: in case the script crashes mid-way,
+ // a re-run can still resolve the project id from the ProjectId doc key.
+ const projectId = await rclient.get(keys.projectKey({ doc_id: docId }))
+ if (projectId) {
+ await rclient.srem(keys.docsInProject({ project_id: projectId }), docId)
+ }
+ await rclient.srem(keys.historyRangesSupport(), docId)
+ await rclient.del(
+ keys.pendingUpdates({ doc_id: docId }),
+ keys.docLines({ doc_id: docId }),
+ keys.projectKey({ doc_id: docId }),
+ keys.docVersion({ doc_id: docId }),
+ keys.docHash({ doc_id: docId }),
+ keys.ranges({ doc_id: docId }),
+ keys.pathname({ doc_id: docId }),
+ keys.projectHistoryId({ doc_id: docId }),
+ keys.unflushedTime({ doc_id: docId }),
+ keys.lastUpdatedBy({ doc_id: docId }),
+ keys.lastUpdatedAt({ doc_id: docId }),
+ keys.resolvedCommentIds({ doc_id: docId }),
+ keys.docOps({ doc_id: docId })
+ )
+}
+
+async function main() {
+ const argv = minimist(process.argv.slice(2), {
+ boolean: ['log-unflushed', 'discard-hard-deleted', 'dry-run', 'help'],
+ string: ['docId', 'projectId'],
+ default: { limit: 100_000, 'log-unflushed': true },
+ })
+
+ if (argv.help) {
+ usage()
+ return 0
+ }
+
+ const options = {
+ estimatedDocs: argv['estimated-docs'],
+ logUnflushed: argv['log-unflushed'],
+ discardHardDeleted: argv['discard-hard-deleted'],
+ docId: argv.docId,
+ projectId: argv.projectId,
+ dryRun: argv['dry-run'],
+ limit: argv.limit,
+ }
+
+ if (!Number.isInteger(options.estimatedDocs) || options.estimatedDocs <= 0) {
+ console.error('--estimated-docs must be set to a positive integer')
+ usage()
+ return 1
+ }
+ if (options.docId && !ObjectId.isValid(options.docId)) {
+ console.error('--docId is not a valid object id')
+ usage()
+ return 1
+ }
+ if (options.projectId && !options.docId) {
+ console.error('--projectId can only be used together with --docId')
+ usage()
+ return 1
+ }
+ if (options.projectId && !ObjectId.isValid(options.projectId)) {
+ console.error('--projectId is not a valid object id')
+ usage()
+ return 1
+ }
+
+ const actualDocs = await db.docs.estimatedDocumentCount()
+ if (
+ actualDocs < 0.8 * options.estimatedDocs ||
+ actualDocs > 1.2 * options.estimatedDocs
+ ) {
+ console.error(
+ `Aborting: the estimated db.docs count of ${actualDocs} is not within 20% of --estimated-docs=${options.estimatedDocs}, is this the right database?`
+ )
+ return 1
+ }
+
+ const docIds = await getDocIds(options)
+ const limitReached = !options.docId && docIds.length >= options.limit
+ for (const docId of docIds) {
+ summary.totalDocs++
+ try {
+ await processDoc(docId, options)
+ } catch (error) {
+ console.error(`Error processing doc ${docId}`)
+ console.error(OError.getFullStack(error))
+ summary.erroredDocs++
+ }
+ }
+
+ if (limitReached) {
+ console.error(
+ `Hit the scan limit of ${options.limit} keys; there may be more docs with pending updates. Re-run to process the rest.`
+ )
+ return 2
+ }
+ if (options.dryRun) {
+ return summary.totalDocs > 0 ? 2 : 0
+ }
+ if (summary.erroredDocs > 0 || summary.missingDocs > summary.discardedDocs) {
+ return 2
+ }
+ return 0
+}
+
+main()
+ .then(code => {
+ printSummary()
+ rclient.quit()
+ mongoClient.close()
+ process.exit(code)
+ })
+ .catch(error => {
+ printSummary()
+ console.error(OError.getFullStack(error))
+ console.error(OError.getFullInfo(error))
+ process.exit(1)
+ })