diff --git a/server-ce/hotfix/6.2.1/Dockerfile b/server-ce/hotfix/6.2.1/Dockerfile new file mode 100644 index 0000000000..e1f968192a --- /dev/null +++ b/server-ce/hotfix/6.2.1/Dockerfile @@ -0,0 +1,14 @@ +FROM sharelatex/sharelatex:6.2.0 + +# [real-time] add strict validation for incoming applyOtUpdate messages +COPY pr_35045.patch . +RUN patch -p1 < pr_35045.patch && rm pr_35045.patch + +# [real-time] gracefully handle undefined meta.tc +COPY pr_35049.patch . +RUN patch -p1 < pr_35049.patch && rm pr_35049.patch + +# [web] avoid sending empty sharejs op +COPY pr_35066.patch . +RUN patch -p1 < pr_35066.patch && rm pr_35066.patch +RUN node genScript compile | bash diff --git a/server-ce/hotfix/6.2.1/pr_35045.patch b/server-ce/hotfix/6.2.1/pr_35045.patch new file mode 100644 index 0000000000..856e723d6a --- /dev/null +++ b/server-ce/hotfix/6.2.1/pr_35045.patch @@ -0,0 +1,49 @@ +diff --git a/services/real-time/app/js/Router.js b/services/real-time/app/js/Router.js +index d25b804a83bc..7bd4864322b5 100644 +--- a/services/real-time/app/js/Router.js ++++ b/services/real-time/app/js/Router.js +@@ -26,7 +26,43 @@ const joinDocSchema = z.object({ + + const applyOtUpdateSchema = z.object({ + doc_id: zz.objectId(), +- update: z.object(), ++ update: z.strictObject({ ++ doc: zz.objectId().optional(), ++ dupIfSource: z.array(z.string()).optional(), ++ hash: z.string().optional(), ++ lastV: z.number().optional(), ++ meta: z ++ .strictObject({ ++ tc: z.string(), ++ }) ++ .optional(), ++ op: z ++ .array( ++ z ++ .strictObject({ ++ i: z.string(), ++ p: z.number().int().min(0), ++ u: z.boolean().optional(), ++ }) ++ .or( ++ z.strictObject({ ++ d: z.string(), ++ p: z.number().int().min(0), ++ u: z.boolean().optional(), ++ }) ++ ) ++ .or( ++ z.strictObject({ ++ c: z.string(), ++ p: z.number().int().min(0), ++ t: z.string(), ++ u: z.boolean().optional(), ++ }) ++ ) ++ ) ++ .min(1), ++ v: z.number().int().min(0), ++ }), + }) + + let Router diff --git a/server-ce/hotfix/6.2.1/pr_35049.patch b/server-ce/hotfix/6.2.1/pr_35049.patch new file mode 100644 index 0000000000..a8d882b350 --- /dev/null +++ b/server-ce/hotfix/6.2.1/pr_35049.patch @@ -0,0 +1,13 @@ +diff --git a/services/real-time/app/js/Router.js b/services/real-time/app/js/Router.js +index e683c0d261ba..7d68a6911a50 100644 +--- a/services/real-time/app/js/Router.js ++++ b/services/real-time/app/js/Router.js +@@ -33,7 +33,7 @@ const applyOtUpdateSchema = z.object({ + lastV: z.number().optional(), + meta: z + .strictObject({ +- tc: z.string(), ++ tc: z.string().optional(), + }) + .optional(), + op: z diff --git a/server-ce/hotfix/6.2.1/pr_35066.patch b/server-ce/hotfix/6.2.1/pr_35066.patch new file mode 100644 index 0000000000..7f027a0ec5 --- /dev/null +++ b/server-ce/hotfix/6.2.1/pr_35066.patch @@ -0,0 +1,75 @@ +diff --git a/services/web/frontend/js/vendor/libs/sharejs.js b/services/web/frontend/js/vendor/libs/sharejs.js +index e3bec4804e22..1749e61e6dfa 100644 +--- a/services/web/frontend/js/vendor/libs/sharejs.js ++++ b/services/web/frontend/js/vendor/libs/sharejs.js +@@ -1048,6 +1048,24 @@ export const { Doc } = (() => { + this.version = msg.v; + } + ++ // Overleaf: an inflight op that cancelled out against a remote op is ++ // empty; there is nothing to resend. Drop it (resolving its callbacks and ++ // clearing the dedup ids) so we do not resend an empty op. If there is no ++ // pending op either, the doc is fully in sync, so emit 'saved'; otherwise ++ // flush() below sends the pending op normally. ++ if (this.inflightOp && this.inflightOp.length === 0) { ++ this.inflightOp = null; ++ var inflightCallbacks = this.inflightCallbacks; ++ this.inflightCallbacks = []; ++ this.inflightSubmittedIds.length = 0; ++ inflightCallbacks.forEach(function (callback) { ++ callback(null); ++ }); ++ if (this.pendingOp === null) { ++ this.emit('saved'); ++ } ++ } ++ + // Resend any previously queued operation. + if (this.inflightOp) { + var response = { +@@ -1145,6 +1163,21 @@ export const { Doc } = (() => { + + this.pendingOp = _Array$from8[0]; + undo = _Array$from8[1]; ++ ++ // Overleaf: a pending op fully reverted by the undo is a no-op. ++ // Drop it (rather than leaving an empty op list) so it is not ++ // flushed and sent as an empty op. Resolve its submit callbacks; the ++ // inflight op was already cleared above, so the doc is fully in ++ // sync: emit 'saved'. ++ if (this.pendingOp.length === 0) { ++ this.pendingOp = null; ++ var pendingCallbacks = this.pendingCallbacks; ++ this.pendingCallbacks = []; ++ pendingCallbacks.forEach(function (callback) { ++ callback(null); ++ }); ++ this.emit('saved'); ++ } + } + + // ... and apply it locally, reverting the changes. +@@ -1261,6 +1294,23 @@ export const { Doc } = (() => { + + this.pendingOp = _Array$from12[0]; + docOp = _Array$from12[1]; ++ ++ // Overleaf: a pending op that fully cancels against the server op is a ++ // no-op. Drop it (rather than leaving an empty op list) so it is not ++ // flushed and sent to the server as an empty op. Resolve its submit ++ // callbacks (it completed as a no-op), and if nothing is inflight either ++ // the doc is fully in sync, so emit 'saved'. ++ if (this.pendingOp.length === 0) { ++ this.pendingOp = null; ++ var pendingCallbacks = this.pendingCallbacks; ++ this.pendingCallbacks = []; ++ pendingCallbacks.forEach(function (callback) { ++ callback(null); ++ }); ++ if (this.inflightOp === null) { ++ this.emit('saved'); ++ } ++ } + } + + this.version++;