diff --git a/server-ce/hotfix/6.2.2/Dockerfile b/server-ce/hotfix/6.2.2/Dockerfile new file mode 100644 index 0000000000..f8f7021370 --- /dev/null +++ b/server-ce/hotfix/6.2.2/Dockerfile @@ -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 diff --git a/server-ce/hotfix/6.2.2/fix_invite_token_secret.patch b/server-ce/hotfix/6.2.2/fix_invite_token_secret.patch new file mode 100644 index 0000000000..6769acc3c7 --- /dev/null +++ b/server-ce/hotfix/6.2.2/fix_invite_token_secret.patch @@ -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, diff --git a/server-ce/hotfix/6.2.2/pr_33656.patch b/server-ce/hotfix/6.2.2/pr_33656.patch new file mode 100644 index 0000000000..45cb1ed364 --- /dev/null +++ b/server-ce/hotfix/6.2.2/pr_33656.patch @@ -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 + diff --git a/server-ce/hotfix/6.2.2/pr_34394.patch b/server-ce/hotfix/6.2.2/pr_34394.patch new file mode 100644 index 0000000000..6cadba60ae --- /dev/null +++ b/server-ce/hotfix/6.2.2/pr_34394.patch @@ -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> +@@ -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')} + + +- {activeGroupSubscriptions && ++ {groupSharingEnabled && ++ activeGroupSubscriptions && + activeGroupSubscriptions.map(subscription => ( + { ++ 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(() => { diff --git a/server-ce/hotfix/6.2.2/pr_34580.patch b/server-ce/hotfix/6.2.2/pr_34580.patch new file mode 100644 index 0000000000..638433b94e --- /dev/null +++ b/server-ce/hotfix/6.2.2/pr_34580.patch @@ -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 + diff --git a/server-ce/hotfix/6.2.2/pr_34916.patch b/server-ce/hotfix/6.2.2/pr_34916.patch new file mode 100644 index 0000000000..a6c89ac629 --- /dev/null +++ b/server-ce/hotfix/6.2.2/pr_34916.patch @@ -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 + diff --git a/server-ce/hotfix/6.2.2/pr_34920.patch b/server-ce/hotfix/6.2.2/pr_34920.patch new file mode 100644 index 0000000000..e9963600af --- /dev/null +++ b/server-ce/hotfix/6.2.2/pr_34920.patch @@ -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", diff --git a/server-ce/hotfix/6.2.2/pr_35531.patch b/server-ce/hotfix/6.2.2/pr_35531.patch new file mode 100644 index 0000000000..4efb326c9a --- /dev/null +++ b/server-ce/hotfix/6.2.2/pr_35531.patch @@ -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' diff --git a/server-ce/hotfix/6.2.2/pr_35603.patch b/server-ce/hotfix/6.2.2/pr_35603.patch new file mode 100644 index 0000000000..e1dc60968e --- /dev/null +++ b/server-ce/hotfix/6.2.2/pr_35603.patch @@ -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) ++ })